Compare commits

..
82 Commits
Author SHA1 Message Date
opencode 585378cba0 release: v1.0.217
Update Nix Hashes / update (push) Has been cancelled
2025-12-30 19:01:36 +00:00
Dax Raad 8cd8393339 core: allow CORS requests from tauri://localhost 2025-12-30 13:58:41 -05:00
GitHub Action b184b2fb73 chore: generate 2025-12-30 18:34:33 +00:00
Aiden Cline c88c2da9be fix: move variant toggle to command bar 2025-12-30 12:33:50 -06:00
opencode 9b04081ae0 release: v1.0.216
Update Nix Hashes / update (push) Has been cancelled
2025-12-30 18:07:37 +00:00
Dax Raad 7d2d87fa2c core: allow CORS requests from *.opencode.ai subdomains 2025-12-30 13:04:18 -05:00
GitHub Action 787f37b382 chore: generate 2025-12-30 17:59:01 +00:00
ja 8fa1af851c style(nix): use idiomatic inherit syntax (#6457) 2025-12-30 11:58:28 -06:00
opencode 73bc3e704e release: v1.0.215
Update Nix Hashes / update (push) Has been cancelled
2025-12-30 17:09:08 +00:00
Adam 8d2feed30e fix(desktop): more defensive agent access 2025-12-30 11:03:34 -06:00
GitHub Action 2d8d4e5dee chore: generate 2025-12-30 16:52:42 +00:00
Fayçal Mitidji b3784588ae Fix: High CPU / memory leak when filtering model list window to empty results (#6435) 2025-12-30 10:52:06 -06:00
opencode 104d52bc38 release: v1.0.214
Update Nix Hashes / update (push) Has been cancelled
2025-12-30 16:37:22 +00:00
Adam dff1fe2d28 fix(desktop): sort servers by health 2025-12-30 10:34:55 -06:00
Adam 72ab4260ee fix(desktop): don't persist fallback server urls 2025-12-30 10:31:48 -06:00
Adam 9e9b4a0555 fix(share): broken share pages 2025-12-30 10:27:06 -06:00
Adam e53192889c fix(app): better text selection 2025-12-30 10:21:37 -06:00
Rohan Mukherjee 23bbfb3d15 fix: cloudflare provider information (#6426) 2025-12-30 09:45:09 -06:00
opencode-agent[bot]andopencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> 37da005a01 docs: projects, find.files, notifications (#6438)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2025-12-30 09:44:09 -06:00
Didier Durand 8b708242f1 chore: fix various typos (#6429) 2025-12-30 09:42:04 -06:00
GitHub Action 339d2dcb98 chore: generate 2025-12-30 15:28:59 +00:00
Adam bbc8678164 fix(desktop): share projects across all local servers 2025-12-30 09:28:19 -06:00
opencode a1d54475fe release: v1.0.213
Update Nix Hashes / update (push) Has been cancelled
2025-12-30 15:26:35 +00:00
Adam 55c601d13a fix(desktop): don't hang on to dead server 2025-12-30 09:24:03 -06:00
Github Action 9115fac4c4 Update Nix flake.lock and hashes 2025-12-30 14:32:38 +00:00
Sebastian Herrlinger cfcb2c1fd8 upgrade opentui to v0.1.67, fixing split diff alignment and markdown jitter 2025-12-30 15:31:02 +01:00
GitHub Action 221fc62135 chore: generate 2025-12-30 13:37:48 +00:00
opencode faaef45384 release: v1.0.212
Update Nix Hashes / update (push) Has been cancelled
2025-12-30 13:37:47 +00:00
Adam 2d18d80ac3 chore: cleanup 2025-12-30 07:35:20 -06:00
Adam e0e07c5d48 feat(app): change server 2025-12-30 07:24:40 -06:00
opencode 281f9e6236 release: v1.0.211
Update Nix Hashes / update (push) Has been cancelled
2025-12-30 13:04:07 +00:00
Github Action f88903a901 Update Nix flake.lock and hashes 2025-12-30 12:58:37 +00:00
ryanwyler ad425a6a6a fix: revert opentui to 0.1.63 to fix streaming jitter regression (#6439) 2025-12-30 13:57:22 +01:00
GitHub Action e635d37027 chore: generate 2025-12-30 12:40:52 +00:00
Connor Adams 97081484d5 docs: global claude skills (#6436) 2025-12-30 06:40:19 -06:00
GitHub Action e451504496 ignore: update download stats 2025-12-30 2025-12-30 12:04:47 +00:00
Github Action 53211c5d37 Update Nix flake.lock and hashes 2025-12-30 11:00:29 +00:00
GitHub Action 98b6817e20 chore: generate 2025-12-30 11:00:29 +00:00
opencode f54d5377a4 release: v1.0.210
Update Nix Hashes / update (push) Has been cancelled
2025-12-30 11:00:28 +00:00
Adam a576fdb5e4 feat(web): open projects 2025-12-30 04:57:37 -06:00
Adam ae53f876f1 feat(desktop): readline shortcuts 2025-12-30 04:57:36 -06:00
Adam a7beba5aa9 chore(desktop): disable sourcemap 2025-12-30 04:57:36 -06:00
Adam e9ef72c20f feat(desktop): more mono (nerd) fonts 2025-12-30 04:57:35 -06:00
Adam fa1ac7bc95 feat(desktop): system notifications 2025-12-30 04:57:35 -06:00
Aiden Cline c82ab649e2 ignore: fix bug from variants pr, prevent createEffect issue 2025-12-30 00:14:10 -06:00
Aiden Cline abc7eed92b tweak: read global claude skills too (#6420) 2025-12-29 23:48:58 -06:00
Joachim IsakssonandJoachim Isaksson 1670d220da fix: prevent model list corruption from SolidJS reactivity (#6359)
Co-authored-by: Joachim Isaksson <joachim.isaksson@centiro.com>
2025-12-29 23:45:15 -06:00
Jkker ddc4e34731 fix(mdns): use named import for bonjour-service (resolves #6422) (#6423) 2025-12-29 23:29:34 -06:00
GitHub Action af99d83709 chore: generate 2025-12-30 03:44:29 +00:00
Aiden ClineandGithub Action ed0c0d90be feat: add variants toggle (#6325)
Co-authored-by: Github Action <action@github.com>
2025-12-29 21:43:50 -06:00
Aiden Cline e1dd9c4ccb ci: improve changelog script 2025-12-29 21:15:36 -06:00
Eduardo Santos de Brito 4657fa823f feat(plugin): expose server URL to plugins (#6373) 2025-12-29 21:05:08 -06:00
opencode-agent[bot]andopencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> 1d589c7ac7 docs: nix formatter (#6414)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2025-12-29 20:54:25 -06:00
Aiden Cline 6b5a0fb261 ci: update token var 2025-12-29 18:16:17 -06:00
Github Action 6d93a7bf55 Update Nix flake.lock and hashes 2025-12-30 00:15:46 +00:00
Sebastian Herrlinger 4ca7ab6be8 upgrade opentui to v0.1.66, fixing split diff alignment 2025-12-30 01:14:18 +01:00
Silvio Ney 713d996b9f fix: Correct theme command in tui.mdx (#6410) 2025-12-29 18:11:07 -06:00
Aiden Cline aa16610021 ci: fix env 2025-12-29 17:52:36 -06:00
GitHub Action d98568fe7e chore: generate 2025-12-29 23:47:03 +00:00
Aiden Cline 1da3550c4d ci: improve err msg 2025-12-29 17:46:29 -06:00
opencode 0c48e6a116 release: v1.0.209
Update Nix Hashes / update (push) Has been cancelled
2025-12-29 23:37:04 +00:00
Adam ef266b2c74 fix(desktop): error page formatting 2025-12-29 17:33:59 -06:00
Aiden Cline 0a1cdc7a58 ci: use .env 2025-12-29 17:31:41 -06:00
Adam 2dec956a17 fix(desktop): better error messages 2025-12-29 17:29:56 -06:00
Aiden Cline ef8388f0ee Revert "feat: read global ~/.claude/skills"
This reverts commit a1c9a1b8c5.
2025-12-29 17:20:04 -06:00
opencode-agent[bot]opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>rekram1-nodeAiden Cline
e5c5b5e872 docs: add run from source guide (#6405)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-29 17:12:42 -06:00
Aiden Cline a1c9a1b8c5 feat: read global ~/.claude/skills 2025-12-29 17:11:28 -06:00
Aiden Cline 76b012139a fix: add timeout to filewatcher subscriptions 2025-12-29 16:16:38 -06:00
Aiden Cline 02e5a19242 tweak: adjust git watcher to ignore files other than HEAD 2025-12-29 16:16:38 -06:00
af967648cb docs: opencode notificator plugin (fixed link) (#6341)
Co-authored-by: Ivan Pantic <panta@talentkit.io>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-29 16:03:35 -06:00
Dominik Engelhardt 504a668a26 Set smallOptions for google models on openrouter (#6362) 2025-12-29 16:01:31 -06:00
ja 5efb1c7b2d feat(highlight): add nix syntax highlighting (#6386) 2025-12-29 15:53:41 -06:00
samcornorandSomair Ansar fd973d242e fix(webfetch): make format parameter optional with markdown default (#6345)
Co-authored-by: Somair Ansar <somairansar@Somairs-MacBook-Air.local>
2025-12-29 15:53:12 -06:00
ja c3d8672753 ci: use env vars for DRY workflow config (#6395) 2025-12-29 15:47:47 -06:00
Shoubhit Dash fe8ef041f6 add supermemory plugin to ecosystem (#6399) 2025-12-29 15:46:22 -06:00
Frank c841de947e zen: add gpt 5.1 codex mini 2025-12-29 16:44:11 -05:00
GitHub Action 825dfd48b1 chore: generate 2025-12-29 21:16:16 +00:00
Graham Bennett 923d114ffa Support different Nix store path prefixes (#6367) 2025-12-29 15:15:42 -06:00
Cole Leavitt b157fd10a7 fix: filter messages with only step-start parts in toModelMessage (#6383) 2025-12-29 14:58:11 -06:00
ja 67ebe68160 feat(format): add nixfmt formatter for Nix files (#6380) 2025-12-29 14:57:52 -06:00
GitHub Action 7b63c14154 chore: generate 2025-12-29 20:50:47 +00:00
Adam cdc11cde2e ignore: hide provider connect button until providers loaded 2025-12-29 14:50:06 -06:00
165 changed files with 2527 additions and 572 deletions
+6 -3
View File
@@ -5,6 +5,9 @@ on:
- cron: "0 */12 * * *" - cron: "0 */12 * * *"
workflow_dispatch: workflow_dispatch:
env:
LOOKBACK_HOURS: 4
jobs: jobs:
update-docs: update-docs:
if: github.repository == 'sst/opencode' if: github.repository == 'sst/opencode'
@@ -25,9 +28,9 @@ jobs:
- name: Get recent commits - name: Get recent commits
id: commits id: commits
run: | 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 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 echo "has_commits=false" >> $GITHUB_OUTPUT
else else
echo "has_commits=true" >> $GITHUB_OUTPUT echo "has_commits=true" >> $GITHUB_OUTPUT
@@ -47,7 +50,7 @@ jobs:
model: opencode/gpt-5.2 model: opencode/gpt-5.2
agent: docs agent: docs
prompt: | 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> <recent_commits>
${{ steps.commits.outputs.list }} ${{ steps.commits.outputs.list }}
+9 -5
View File
@@ -5,6 +5,10 @@ on:
- cron: "30 1 * * *" # Daily at 1:30 AM - cron: "30 1 * * *" # Daily at 1:30 AM
workflow_dispatch: workflow_dispatch:
env:
DAYS_BEFORE_STALE: 90
DAYS_BEFORE_CLOSE: 7
jobs: jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -13,17 +17,17 @@ jobs:
steps: steps:
- uses: actions/stale@v10 - uses: actions/stale@v10
with: with:
days-before-stale: 90 days-before-stale: ${{ env.DAYS_BEFORE_STALE }}
days-before-close: 7 days-before-close: ${{ env.DAYS_BEFORE_CLOSE }}
stale-issue-label: "stale" stale-issue-label: "stale"
close-issue-message: | 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! Feel free to reopen if you still need this!
stale-issue-message: | 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 remove-stale-when-updated: true
exempt-issue-labels: "pinned,security,feature-request,on-hold" exempt-issue-labels: "pinned,security,feature-request,on-hold"
start-date: "2025-12-27" start-date: "2025-12-27"
+1
View File
@@ -32,3 +32,4 @@ jobs:
./script/sync-zed.ts ${{ steps.get_tag.outputs.tag }} ./script/sync-zed.ts ${{ steps.get_tag.outputs.tag }}
env: env:
ZED_EXTENSIONS_PAT: ${{ secrets.ZED_EXTENSIONS_PAT }} ZED_EXTENSIONS_PAT: ${{ secrets.ZED_EXTENSIONS_PAT }}
ZED_PR_PAT: ${{ secrets.ZED_PR_PAT }}
+4
View File
@@ -20,3 +20,7 @@ opencode.json
a.out a.out
target target
.scripts .scripts
# Local dev files
opencode-dev
logs/
+30
View File
@@ -34,6 +34,36 @@ Want to take on an issue? Leave a comment and a maintainer may assign it to you
bun dev 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: - Core pieces:
- `packages/opencode`: OpenCode core business logic & server. - `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) - `packages/opencode/src/cli/cmd/tui/`: The TUI code, written in SolidJS with [opentui](https://github.com/sst/opentui)
+1
View File
@@ -185,3 +185,4 @@
| 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) | | 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) |
| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) | | 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) |
| 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) | | 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) |
| 2025-12-30 | 1,445,450 (+29,890) | 1,272,689 (+15,588) | 2,718,139 (+45,478) |
+35 -43
View File
@@ -22,7 +22,7 @@
}, },
"packages/app": { "packages/app": {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.0.208", "version": "1.0.217",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@@ -70,7 +70,7 @@
}, },
"packages/console/app": { "packages/console/app": {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.0.208", "version": "1.0.217",
"dependencies": { "dependencies": {
"@cloudflare/vite-plugin": "1.15.2", "@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",
@@ -98,7 +98,7 @@
}, },
"packages/console/core": { "packages/console/core": {
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.0.208", "version": "1.0.217",
"dependencies": { "dependencies": {
"@aws-sdk/client-sts": "3.782.0", "@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1", "@jsx-email/render": "1.1.1",
@@ -125,7 +125,7 @@
}, },
"packages/console/function": { "packages/console/function": {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.0.208", "version": "1.0.217",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "2.0.0", "@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2", "@ai-sdk/openai": "2.0.2",
@@ -149,7 +149,7 @@
}, },
"packages/console/mail": { "packages/console/mail": {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.0.208", "version": "1.0.217",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
@@ -173,13 +173,14 @@
}, },
"packages/desktop": { "packages/desktop": {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"version": "1.0.208", "version": "1.0.217",
"dependencies": { "dependencies": {
"@opencode-ai/app": "workspace:*", "@opencode-ai/app": "workspace:*",
"@solid-primitives/storage": "catalog:", "@solid-primitives/storage": "catalog:",
"@tauri-apps/api": "^2", "@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "~2", "@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-http": "~2", "@tauri-apps/plugin-http": "~2",
"@tauri-apps/plugin-notification": "~2",
"@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-os": "~2", "@tauri-apps/plugin-os": "~2",
"@tauri-apps/plugin-process": "~2", "@tauri-apps/plugin-process": "~2",
@@ -200,7 +201,7 @@
}, },
"packages/enterprise": { "packages/enterprise": {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.0.208", "version": "1.0.217",
"dependencies": { "dependencies": {
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*", "@opencode-ai/util": "workspace:*",
@@ -229,7 +230,7 @@
}, },
"packages/function": { "packages/function": {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.0.208", "version": "1.0.217",
"dependencies": { "dependencies": {
"@octokit/auth-app": "8.0.1", "@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:", "@octokit/rest": "catalog:",
@@ -245,7 +246,7 @@
}, },
"packages/opencode": { "packages/opencode": {
"name": "opencode", "name": "opencode",
"version": "1.0.208", "version": "1.0.217",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@@ -255,18 +256,17 @@
"@agentclientprotocol/sdk": "0.5.1", "@agentclientprotocol/sdk": "0.5.1",
"@ai-sdk/amazon-bedrock": "3.0.57", "@ai-sdk/amazon-bedrock": "3.0.57",
"@ai-sdk/anthropic": "2.0.56", "@ai-sdk/anthropic": "2.0.56",
"@ai-sdk/azure": "2.0.73", "@ai-sdk/azure": "2.0.82",
"@ai-sdk/cerebras": "1.0.33", "@ai-sdk/cerebras": "1.0.33",
"@ai-sdk/cohere": "2.0.21", "@ai-sdk/cohere": "2.0.21",
"@ai-sdk/deepinfra": "1.0.30", "@ai-sdk/deepinfra": "1.0.30",
"@ai-sdk/gateway": "2.0.23", "@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/google-vertex": "3.0.81",
"@ai-sdk/groq": "2.0.33", "@ai-sdk/groq": "2.0.33",
"@ai-sdk/mcp": "0.0.8",
"@ai-sdk/mistral": "2.0.26", "@ai-sdk/mistral": "2.0.26",
"@ai-sdk/openai": "2.0.71", "@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/perplexity": "2.0.22",
"@ai-sdk/provider": "2.0.0", "@ai-sdk/provider": "2.0.0",
"@ai-sdk/provider-utils": "3.0.19", "@ai-sdk/provider-utils": "3.0.19",
@@ -284,8 +284,8 @@
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*", "@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "1.5.2", "@openrouter/ai-sdk-provider": "1.5.2",
"@opentui/core": "0.1.63", "@opentui/core": "0.1.67",
"@opentui/solid": "0.1.63", "@opentui/solid": "0.1.67",
"@parcel/watcher": "2.5.1", "@parcel/watcher": "2.5.1",
"@pierre/diffs": "catalog:", "@pierre/diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2", "@solid-primitives/event-bus": "1.1.2",
@@ -347,7 +347,7 @@
}, },
"packages/plugin": { "packages/plugin": {
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.0.208", "version": "1.0.217",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"zod": "catalog:", "zod": "catalog:",
@@ -367,7 +367,7 @@
}, },
"packages/sdk/js": { "packages/sdk/js": {
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "1.0.208", "version": "1.0.217",
"devDependencies": { "devDependencies": {
"@hey-api/openapi-ts": "0.88.1", "@hey-api/openapi-ts": "0.88.1",
"@tsconfig/node22": "catalog:", "@tsconfig/node22": "catalog:",
@@ -378,7 +378,7 @@
}, },
"packages/slack": { "packages/slack": {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.0.208", "version": "1.0.217",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1", "@slack/bolt": "^3.17.1",
@@ -391,7 +391,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "1.0.208", "version": "1.0.217",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@@ -429,7 +429,7 @@
}, },
"packages/util": { "packages/util": {
"name": "@opencode-ai/util", "name": "@opencode-ai/util",
"version": "1.0.208", "version": "1.0.217",
"dependencies": { "dependencies": {
"zod": "catalog:", "zod": "catalog:",
}, },
@@ -440,7 +440,7 @@
}, },
"packages/web": { "packages/web": {
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"version": "1.0.208", "version": "1.0.217",
"dependencies": { "dependencies": {
"@astrojs/cloudflare": "12.6.3", "@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1", "@astrojs/markdown-remark": "6.3.1",
@@ -541,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/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=="], "@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=="],
@@ -551,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/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/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/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/mistral": ["@ai-sdk/mistral@2.0.26", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jxDB++4WI1wEx5ONNBI+VbkmYJOYIuS8UQY13/83UGRaiW7oB/WHiH4ETe6KzbKpQPB3XruwTJQjUMsMfKyTXA=="],
"@ai-sdk/openai": ["@ai-sdk/openai@2.0.2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-D4zYz2uR90aooKQvX1XnS00Z7PkbrcY+snUvPfm5bCabTG7bzLrVtD56nJ5bSaZG8lmuOMfXpyiEEArYLyWPpw=="], "@ai-sdk/openai": ["@ai-sdk/openai@2.0.2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-D4zYz2uR90aooKQvX1XnS00Z7PkbrcY+snUvPfm5bCabTG7bzLrVtD56nJ5bSaZG8lmuOMfXpyiEEArYLyWPpw=="],
@@ -1195,21 +1193,21 @@
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@opentui/core": ["@opentui/core@0.1.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=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
@@ -1709,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-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-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=="], "@tauri-apps/plugin-os": ["@tauri-apps/plugin-os@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A=="],
@@ -3901,22 +3901,16 @@
"@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="], "@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
"@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/openai@2.0.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/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/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/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/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
"@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="], "@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
"@ai-sdk/google/@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/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/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/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/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/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/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="], "@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
"@ai-sdk/openai-compatible/@ai-sdk/provider-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=="],
@@ -4309,7 +4303,7 @@
"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": ["@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=="], "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=="],
@@ -4913,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/@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/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
"opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="], "opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="],
Generated
+3 -3
View File
@@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1766996594, "lastModified": 1767026758,
"narHash": "sha256-SosfgQSqVmOkqVgNYJnxW5FvoIQX4grOcpIKNrIwz4o=", "narHash": "sha256-7fsac/f7nh/VaKJ/qm3I338+wAJa/3J57cOGpXi0Sbg=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "0744ef1b047f07d31d9962d757ffe38ec14a4d41", "rev": "346dd96ad74dc4457a9db9de4f4f57dab2e5731d",
"type": "github" "type": "github"
}, },
"original": { "original": {
+3 -3
View File
@@ -17,7 +17,7 @@
"aarch64-darwin" "aarch64-darwin"
"x86_64-darwin" "x86_64-darwin"
]; ];
lib = nixpkgs.lib; inherit (nixpkgs) lib;
forEachSystem = lib.genAttrs systems; forEachSystem = lib.genAttrs systems;
pkgsFor = system: nixpkgs.legacyPackages.${system}; pkgsFor = system: nixpkgs.legacyPackages.${system};
packageJson = builtins.fromJSON (builtins.readFile ./packages/opencode/package.json); packageJson = builtins.fromJSON (builtins.readFile ./packages/opencode/package.json);
@@ -70,12 +70,12 @@
in in
{ {
default = mkPackage { default = mkPackage {
version = packageJson.version; inherit (packageJson) version;
src = ./.; src = ./.;
scripts = ./nix/scripts; scripts = ./nix/scripts;
target = bunTarget.${system}; target = bunTarget.${system};
modelsDev = "${modelsDev.${system}}/dist/_api.json"; modelsDev = "${modelsDev.${system}}/dist/_api.json";
mkNodeModules = mkNodeModules; inherit mkNodeModules;
}; };
} }
); );
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"nodeModules": "sha256-2i/QMBzp9MalOXur36mXaDDU8R9G/0dODCODEQOnaCU=" "nodeModules": "sha256-2Wbnxy9SPcZkO03Sis3uiypPXa87jc5TzKbo6PvMlxY="
} }
+18 -10
View File
@@ -1,18 +1,26 @@
{ hash, lib, stdenvNoCC, bun, cacert, curl }: {
hash,
lib,
stdenvNoCC,
bun,
cacert,
curl,
}:
args: args:
stdenvNoCC.mkDerivation { stdenvNoCC.mkDerivation {
pname = "opencode-node_modules"; pname = "opencode-node_modules";
version = args.version; inherit (args) version src;
src = args.src;
impureEnvVars = impureEnvVars = lib.fetchers.proxyImpureEnvVars ++ [
lib.fetchers.proxyImpureEnvVars "GIT_PROXY_COMMAND"
++ [ "SOCKS_SERVER"
"GIT_PROXY_COMMAND" ];
"SOCKS_SERVER"
];
nativeBuildInputs = [ bun cacert curl ]; nativeBuildInputs = [
bun
cacert
curl
];
dontConfigure = true; dontConfigure = true;
+10 -7
View File
@@ -1,7 +1,13 @@
{ lib, stdenvNoCC, bun, ripgrep, makeBinaryWrapper }: {
lib,
stdenvNoCC,
bun,
ripgrep,
makeBinaryWrapper,
}:
args: args:
let let
scripts = args.scripts; inherit (args) scripts;
mkModules = mkModules =
attrs: attrs:
args.mkNodeModules ( args.mkNodeModules (
@@ -14,13 +20,10 @@ let
in in
stdenvNoCC.mkDerivation (finalAttrs: { stdenvNoCC.mkDerivation (finalAttrs: {
pname = "opencode"; pname = "opencode";
version = args.version; inherit (args) version src;
src = args.src;
node_modules = mkModules { node_modules = mkModules {
version = finalAttrs.version; inherit (finalAttrs) version src;
src = finalAttrs.src;
}; };
nativeBuildInputs = [ nativeBuildInputs = [
+7 -3
View File
@@ -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) 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/...") // 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(/(\.\/)+/g, "./")
next = next.replace(/(\.\.\/)+\/?(\/nix\/store[^"']+)/g, "/$2") next = next.replace(
next = next.replace(/(["'])\/{2,}(\/nix\/store[^"']+)(["'])/g, "$1/$2$3") new RegExp(`(\\.\\.\\/)+\\/{1,2}(${nixStorePrefix.replace(/^\//, "").replace(/\//g, "\\/")}[^"']+)`, "g"),
next = next.replace(/(["'])\/\/(nix\/store[^"']+)(["'])/g, "$1/$2$3") "/$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) if (next !== content) fs.writeFileSync(file, next)
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.0.208", "version": "1.0.217",
"description": "", "description": "",
"type": "module", "type": "module",
"exports": { "exports": {
+47 -33
View File
@@ -1,5 +1,5 @@
import "@/index.css" 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 { Router, Route, Navigate } from "@solidjs/router"
import { MetaProvider } from "@solidjs/meta" import { MetaProvider } from "@solidjs/meta"
import { Font } from "@opencode-ai/ui/font" 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 { GlobalSyncProvider } from "@/context/global-sync"
import { LayoutProvider } from "@/context/layout" import { LayoutProvider } from "@/context/layout"
import { GlobalSDKProvider } from "@/context/global-sdk" import { GlobalSDKProvider } from "@/context/global-sdk"
import { ServerProvider, useServer } from "@/context/server"
import { TerminalProvider } from "@/context/terminal" import { TerminalProvider } from "@/context/terminal"
import { PromptProvider } from "@/context/prompt" import { PromptProvider } from "@/context/prompt"
import { NotificationProvider } from "@/context/notification" 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") const param = new URLSearchParams(document.location.search).get("url")
if (param) return param if (param) return param
@@ -42,6 +43,15 @@ const url = iife(() => {
return window.location.origin return window.location.origin
}) })
function ServerKey(props: ParentProps) {
const server = useServer()
return (
<Show when={server.url} keyed>
{props.children}
</Show>
)
}
export function App() { export function App() {
return ( return (
<MetaProvider> <MetaProvider>
@@ -52,38 +62,42 @@ export function App() {
<MarkedProvider> <MarkedProvider>
<DiffComponentProvider component={Diff}> <DiffComponentProvider component={Diff}>
<CodeComponentProvider component={Code}> <CodeComponentProvider component={Code}>
<GlobalSDKProvider url={url}> <ServerProvider defaultUrl={defaultServerUrl}>
<GlobalSyncProvider> <ServerKey>
<LayoutProvider> <GlobalSDKProvider>
<NotificationProvider> <GlobalSyncProvider>
<Router <LayoutProvider>
root={(props) => ( <NotificationProvider>
<CommandProvider> <Router
<Layout>{props.children}</Layout> root={(props) => (
</CommandProvider> <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 ?? "new"} keyed>
<TerminalProvider>
<PromptProvider>
<Session />
</PromptProvider>
</TerminalProvider>
</Show>
)} )}
/> >
</Route> <Route path="/" component={Home} />
</Router> <Route path="/:dir" component={DirectoryLayout}>
</NotificationProvider> <Route path="/" component={() => <Navigate href="session" />} />
</LayoutProvider> <Route
</GlobalSyncProvider> path="/session/:id?"
</GlobalSDKProvider> component={(p) => (
<Show when={p.params.id ?? "new"} keyed>
<TerminalProvider>
<PromptProvider>
<Session />
</PromptProvider>
</TerminalProvider>
</Show>
)}
/>
</Route>
</Router>
</NotificationProvider>
</LayoutProvider>
</GlobalSyncProvider>
</GlobalSDKProvider>
</ServerKey>
</ServerProvider>
</CodeComponentProvider> </CodeComponentProvider>
</DiffComponentProvider> </DiffComponentProvider>
</MarkedProvider> </MarkedProvider>
@@ -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>
)
}
+243 -5
View File
@@ -134,6 +134,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
imageAttachments: ImageAttachmentPart[] imageAttachments: ImageAttachmentPart[]
mode: "normal" | "shell" mode: "normal" | "shell"
applyingHistory: boolean applyingHistory: boolean
killBuffer: string
}>({ }>({
popover: null, popover: null,
historyIndex: -1, historyIndex: -1,
@@ -143,6 +144,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
imageAttachments: [], imageAttachments: [],
mode: "normal", mode: "normal",
applyingHistory: false, applyingHistory: false,
killBuffer: "",
}) })
const MAX_HISTORY = 100 const MAX_HISTORY = 100
@@ -648,6 +650,77 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
setStore("popover", null) 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 = () => const abort = () =>
sdk.client.session sdk.client.session
.abort({ .abort({
@@ -768,6 +841,164 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
return 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.key === "ArrowUp" || event.key === "ArrowDown") {
if (event.altKey || event.ctrlKey || event.metaKey) return if (event.altKey || event.ctrlKey || event.metaKey) return
const { collapsed } = getCaretState() const { collapsed } = getCaretState()
@@ -884,11 +1115,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
setStore("imageAttachments", []) setStore("imageAttachments", [])
setStore("mode", "normal") setStore("mode", "normal")
const model = { const currentModel = local.model.current()
modelID: local.model.current()!.id, const currentAgent = local.agent.current()
providerID: local.model.current()!.provider.id, 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) { if (isShellMode) {
sdk.client.session sdk.client.session
@@ -1090,6 +1327,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
onInput={handleInput} onInput={handleInput}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
classList={{ 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, "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, "[&_[data-type=file]]:text-icon-info-active": true,
"font-mono!": store.mode === "shell", "font-mono!": store.mode === "shell",
@@ -1128,7 +1366,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
> >
<Select <Select
options={local.agent.list().map((agent) => agent.name)} options={local.agent.list().map((agent) => agent.name)}
current={local.agent.current().name} current={local.agent.current()?.name ?? ""}
onSelect={local.agent.set} onSelect={local.agent.set}
class="capitalize" class="capitalize"
variant="ghost" variant="ghost"
@@ -1,5 +1,4 @@
import { createMemo, Show } from "solid-js" import { createMemo, Show } from "solid-js"
import { Icon } from "@opencode-ai/ui/icon"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
import { Tooltip } from "@opencode-ai/ui/tooltip" import { Tooltip } from "@opencode-ai/ui/tooltip"
@@ -24,12 +23,11 @@ export function SessionLspIndicator() {
<Show when={lspStats().total > 0}> <Show when={lspStats().total > 0}>
<Tooltip placement="top" value={tooltipContent()}> <Tooltip placement="top" value={tooltipContent()}>
<div class="flex items-center gap-1 px-2 cursor-default select-none"> <div class="flex items-center gap-1 px-2 cursor-default select-none">
<Icon <div
name="code"
size="small"
classList={{ classList={{
"text-icon-critical-base": lspStats().hasError, "size-1.5 rounded-full": true,
"text-icon-success-base": !lspStats().hasError && lspStats().connected > 0, "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> <span class="text-12-regular text-text-weak">{lspStats().connected} LSP</span>
@@ -1,6 +1,5 @@
import { createMemo, Show } from "solid-js" import { createMemo, Show } from "solid-js"
import { Button } from "@opencode-ai/ui/button" import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { useDialog } from "@opencode-ai/ui/context/dialog" import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
import { DialogSelectMcp } from "@/components/dialog-select-mcp" import { DialogSelectMcp } from "@/components/dialog-select-mcp"
@@ -21,12 +20,11 @@ export function SessionMcpIndicator() {
return ( return (
<Show when={mcpStats().total > 0}> <Show when={mcpStats().total > 0}>
<Button variant="ghost" onClick={() => dialog.show(() => <DialogSelectMcp />)}> <Button variant="ghost" onClick={() => dialog.show(() => <DialogSelectMcp />)}>
<Icon <div
name="mcp"
size="small"
classList={{ classList={{
"text-icon-critical-base": mcpStats().failed, "size-1.5 rounded-full": true,
"text-icon-success-base": !mcpStats().failed && mcpStats().enabled > 0, "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> <span class="text-12-regular text-text-weak">{mcpStats().enabled} MCP</span>
+26 -5
View File
@@ -1,10 +1,14 @@
import { createMemo, Show, type ParentProps } from "solid-js" import { createMemo, Show, type ParentProps } from "solid-js"
import { usePlatform } from "@/context/platform"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
import { useGlobalSync } from "@/context/global-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) { export function StatusBar(props: ParentProps) {
const platform = usePlatform() const dialog = useDialog()
const server = useServer()
const sync = useSync() const sync = useSync()
const globalSync = useGlobalSync() const globalSync = useGlobalSync()
@@ -19,9 +23,26 @@ export function StatusBar(props: ParentProps) {
return ( 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="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"> <div class="flex items-center gap-3">
<Show when={platform.version}> <div class="flex items-center gap-1">
<span class="text-12-regular text-text-weak">v{platform.version}</span> <Button
</Show> 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()}> <Show when={directoryDisplay()}>
<span class="text-12-regular text-text-weak">{directoryDisplay()}</span> <span class="text-12-regular text-text-weak">{directoryDisplay()}</span>
</Show> </Show>
+1
View File
@@ -233,6 +233,7 @@ export const Terminal = (props: TerminalProps) => {
style={{ "background-color": terminalColors().background }} style={{ "background-color": terminalColors().background }}
classList={{ classList={{
...(local.classList ?? {}), ...(local.classList ?? {}),
"select-text": true,
"size-full px-6 py-3 font-mono": true, "size-full px-6 py-3 font-mono": true,
[local.class ?? ""]: !!local.class, [local.class ?? ""]: !!local.class,
}} }}
+15 -8
View File
@@ -1,34 +1,41 @@
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client" import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
import { createSimpleContext } from "@opencode-ai/ui/context" import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus" import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { onCleanup } from "solid-js"
import { usePlatform } from "./platform" import { usePlatform } from "./platform"
import { useServer } from "./server"
export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({
name: "GlobalSDK", name: "GlobalSDK",
init: (props: { url: string }) => { init: () => {
const server = useServer()
const abort = new AbortController()
const eventSdk = createOpencodeClient({ const eventSdk = createOpencodeClient({
baseUrl: props.url, baseUrl: server.url,
// signal: AbortSignal.timeout(1000 * 60 * 10), signal: abort.signal,
}) })
const emitter = createGlobalEmitter<{ const emitter = createGlobalEmitter<{
[key: string]: Event [key: string]: Event
}>() }>()
eventSdk.global.event().then(async (events) => { void (async () => {
const events = await eventSdk.global.event()
for await (const event of events.stream) { for await (const event of events.stream) {
// console.log("event", event)
emitter.emit(event.directory ?? "global", event.payload) emitter.emit(event.directory ?? "global", event.payload)
} }
}) })().catch(() => undefined)
onCleanup(() => abort.abort())
const platform = usePlatform() const platform = usePlatform()
const sdk = createOpencodeClient({ const sdk = createOpencodeClient({
baseUrl: props.url, baseUrl: server.url,
signal: AbortSignal.timeout(1000 * 60 * 10), signal: AbortSignal.timeout(1000 * 60 * 10),
fetch: platform.fetch, fetch: platform.fetch,
throwOnError: true, throwOnError: true,
}) })
return { url: props.url, client: sdk, event: emitter } return { url: server.url, client: sdk, event: emitter }
}, },
}) })
+11 -19
View File
@@ -3,6 +3,7 @@ import { batch, createMemo, onMount } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context" import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSync } from "./global-sync" import { useGlobalSync } from "./global-sync"
import { useGlobalSDK } from "./global-sdk" import { useGlobalSDK } from "./global-sdk"
import { useServer } from "./server"
import { Project } from "@opencode-ai/sdk/v2" import { Project } from "@opencode-ai/sdk/v2"
import { persisted } from "@/utils/persist" import { persisted } from "@/utils/persist"
@@ -34,10 +35,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
init: () => { init: () => {
const globalSdk = useGlobalSDK() const globalSdk = useGlobalSDK()
const globalSync = useGlobalSync() const globalSync = useGlobalSync()
const server = useServer()
const [store, setStore, _, ready] = persisted( const [store, setStore, _, ready] = persisted(
"layout.v3", "layout.v4",
createStore({ createStore({
projects: [] as { worktree: string; expanded: boolean }[],
sidebar: { sidebar: {
opened: false, opened: false,
width: 280, width: 280,
@@ -86,12 +87,12 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
return project return project
} }
const enriched = createMemo(() => store.projects.flatMap(enrich)) const enriched = createMemo(() => server.projects.list().flatMap(enrich))
const list = createMemo(() => enriched().flatMap(colorize)) const list = createMemo(() => enriched().flatMap(colorize))
onMount(() => { onMount(() => {
Promise.all( Promise.all(
store.projects.map((project) => { server.projects.list().map((project) => {
return globalSync.project.loadSessions(project.worktree) return globalSync.project.loadSessions(project.worktree)
}), }),
) )
@@ -102,32 +103,23 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
projects: { projects: {
list, list,
open(directory: string) { open(directory: string) {
if (store.projects.find((x) => x.worktree === directory)) { if (server.projects.list().find((x) => x.worktree === directory)) {
return return
} }
globalSync.project.loadSessions(directory) globalSync.project.loadSessions(directory)
setStore("projects", (x) => [{ worktree: directory, expanded: true }, ...x]) server.projects.open(directory)
}, },
close(directory: string) { close(directory: string) {
setStore("projects", (x) => x.filter((x) => x.worktree !== directory)) server.projects.close(directory)
}, },
expand(directory: string) { expand(directory: string) {
const index = store.projects.findIndex((x) => x.worktree === directory) server.projects.expand(directory)
if (index !== -1) setStore("projects", index, "expanded", true)
}, },
collapse(directory: string) { collapse(directory: string) {
const index = store.projects.findIndex((x) => x.worktree === directory) server.projects.collapse(directory)
if (index !== -1) setStore("projects", index, "expanded", false)
}, },
move(directory: string, toIndex: number) { move(directory: string, toIndex: number) {
setStore("projects", (projects) => { server.projects.move(directory, toIndex)
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
})
}, },
}, },
sidebar: { sidebar: {
+30 -10
View File
@@ -65,23 +65,40 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const agent = (() => { const agent = (() => {
const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden)) const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden))
const [store, setStore] = createStore<{ const [store, setStore] = createStore<{
current: string current?: string
}>({ }>({
current: list()[0].name, current: list()[0]?.name,
}) })
return { return {
list, list,
current() { 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) { 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) { move(direction: 1 | -1) {
let next = list().findIndex((x) => x.name === store.current) + direction const available = list()
if (next < 0) next = list().length - 1 if (available.length === 0) {
if (next >= list().length) next = 0 setStore("current", undefined)
const value = list()[next] 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) setStore("current", value.name)
if (value.model) if (value.model)
model.set({ model.set({
@@ -182,11 +199,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const current = createMemo(() => { const current = createMemo(() => {
const a = agent.current() const a = agent.current()
if (!a) return undefined
const key = getFirstValidModel( const key = getFirstValidModel(
() => ephemeral.model[a.name], () => ephemeral.model[a.name],
() => a.model, () => a.model,
fallbackModel, fallbackModel,
)! )
if (!key) return undefined
return find(key) return find(key)
}) })
@@ -232,7 +251,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
cycle, cycle,
set(model: ModelKey | undefined, options?: { recent?: boolean }) { set(model: ModelKey | undefined, options?: { recent?: boolean }) {
batch(() => { 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 (model) updateVisibility(model, "show")
if (options?.recent && model) { if (options?.recent && model) {
const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID) const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID)
+16 -9
View File
@@ -2,7 +2,9 @@ import { createStore } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context" import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSDK } from "./global-sdk" import { useGlobalSDK } from "./global-sdk"
import { useGlobalSync } from "./global-sync" import { useGlobalSync } from "./global-sync"
import { usePlatform } from "@/context/platform"
import { Binary } from "@opencode-ai/util/binary" import { Binary } from "@opencode-ai/util/binary"
import { base64Encode } from "@opencode-ai/util/encode"
import { EventSessionError } from "@opencode-ai/sdk/v2" import { EventSessionError } from "@opencode-ai/sdk/v2"
import { makeAudioPlayer } from "@solid-primitives/audio" import { makeAudioPlayer } from "@solid-primitives/audio"
import idleSound from "@opencode-ai/ui/audio/staplebops-01.aac" 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 globalSDK = useGlobalSDK()
const globalSync = useGlobalSync() const globalSync = useGlobalSync()
const platform = usePlatform()
const [store, setStore, _, ready] = persisted( const [store, setStore, _, ready] = persisted(
"notification.v1", "notification.v1",
@@ -64,8 +67,8 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
const sessionID = event.properties.sessionID const sessionID = event.properties.sessionID
const [syncStore] = globalSync.child(directory) const [syncStore] = globalSync.child(directory)
const match = Binary.search(syncStore.session, sessionID, (s) => s.id) const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
const isChild = match.found && syncStore.session[match.index].parentID const session = match.found ? syncStore.session[match.index] : undefined
if (isChild) break if (session?.parentID) break
try { try {
idlePlayer?.play() idlePlayer?.play()
} catch {} } catch {}
@@ -74,25 +77,29 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
type: "turn-complete", type: "turn-complete",
session: sessionID, session: sessionID,
}) })
const href = `/${base64Encode(directory)}/session/${sessionID}`
void platform.notify("Response ready", session?.title ?? sessionID, href)
break break
} }
case "session.error": { case "session.error": {
const sessionID = event.properties.sessionID const sessionID = event.properties.sessionID
if (sessionID) { const [syncStore] = globalSync.child(directory)
const [syncStore] = globalSync.child(directory) const match = sessionID ? Binary.search(syncStore.session, sessionID, (s) => s.id) : undefined
const match = Binary.search(syncStore.session, sessionID, (s) => s.id) const session = sessionID && match?.found ? syncStore.session[match.index] : undefined
const isChild = match.found && syncStore.session[match.index].parentID if (session?.parentID) break
if (isChild) break
}
try { try {
errorPlayer?.play() errorPlayer?.play()
} catch {} } catch {}
const error = "error" in event.properties ? event.properties.error : undefined
setStore("list", store.list.length, { setStore("list", store.list.length, {
...base, ...base,
type: "error", type: "error",
session: sessionID ?? "global", 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 break
} }
} }
+4 -1
View File
@@ -14,7 +14,10 @@ export type Platform = {
/** Restart the app */ /** Restart the app */
restart(): Promise<void> 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> openDirectoryPickerDialog?(opts?: { title?: string; multiple?: boolean }): Promise<string | string[] | null>
/** Open native file picker dialog (Tauri only) */ /** Open native file picker dialog (Tauri only) */
+183
View File
@@ -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)
},
},
}
},
})
+30
View File
@@ -20,6 +20,36 @@ const platform: Platform = {
restart: async () => { restart: async () => {
window.location.reload() 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( render(
+83 -20
View File
@@ -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 { function formatInitError(error: InitError): string {
const data = error.data const data = error.data
switch (error.name) { switch (error.name) {
case "MCPFailed": case "MCPFailed":
return `MCP server "${data.name}" failed. Note, opencode does not support MCP authentication yet.` 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": { case "ProviderModelNotFoundError": {
const { providerID, modelID, suggestions } = data as { const { providerID, modelID, suggestions } = data as {
providerID: string providerID: string
@@ -37,10 +77,14 @@ function formatInitError(error: InitError): string {
`Check your config (opencode.json) provider/model names`, `Check your config (opencode.json) provider/model names`,
].join("\n") ].join("\n")
} }
case "ProviderInitError": case "ProviderInitError": {
return `Failed to initialize provider "${data.providerID}". Check credentials and configuration.` const providerID = typeof data.providerID === "string" ? data.providerID : "unknown"
case "ConfigJsonError": return `Failed to initialize provider "${providerID}". Check credentials and configuration.`
return `Config file at ${data.path} is not valid JSON(C)` + (data.message ? `: ${data.message}` : "") }
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": 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.` 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": case "ConfigFrontmatterError":
@@ -51,14 +95,14 @@ function formatInitError(error: InitError): string {
(issue: { message: string; path: string[] }) => "↳ " + issue.message + " " + issue.path.join("."), (issue: { message: string; path: string[] }) => "↳ " + issue.message + " " + issue.path.join("."),
) )
: [] : []
return [`Config file at ${data.path} is invalid` + (data.message ? `: ${data.message}` : ""), ...issues].join( const message = typeof data.message === "string" ? data.message : ""
"\n", return [`Config file at ${data.path} is invalid` + (message ? `: ${message}` : ""), ...issues].join("\n")
)
} }
case "UnknownError": case "UnknownError":
return String(data.message) return typeof data.message === "string" ? data.message : safeJson(data)
default: 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) const message = formatInitError(error)
if (depth > 0 && parentMessage === message) return "" if (depth > 0 && parentMessage === message) return ""
const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : "" const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
return indent + message return indent + `${error.name}\n${message}`
} }
if (error instanceof Error) { if (error instanceof Error) {
@@ -77,15 +121,34 @@ function formatErrorChain(error: unknown, depth = 0, parentMessage?: string): st
const parts: string[] = [] const parts: string[] = []
const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : "" const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
if (!isDuplicate) { const header = `${error.name}${error.message ? `: ${error.message}` : ""}`
// Stack already includes error name and message, so prefer it const stack = error.stack?.trim()
parts.push(indent + (error.stack ?? `${error.name}: ${error.message}`))
} else if (error.stack) { if (stack) {
// Duplicate message - only show the stack trace lines (skip message) const startsWithHeader = stack.startsWith(header)
const trace = error.stack.split("\n").slice(1).join("\n").trim()
if (trace) { if (isDuplicate && startsWithHeader) {
parts.push(trace) 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) { 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` : "" 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 { function formatError(error: unknown): string {
@@ -131,7 +194,7 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
readOnly readOnly
copyable copyable
multiline 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" label="Error Details"
hideLabel hideLabel
/> />
+48 -19
View File
@@ -8,12 +8,18 @@ import { base64Encode } from "@opencode-ai/util/encode"
import { Icon } from "@opencode-ai/ui/icon" import { Icon } from "@opencode-ai/ui/icon"
import { usePlatform } from "@/context/platform" import { usePlatform } from "@/context/platform"
import { DateTime } from "luxon" 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() { export default function Home() {
const sync = useGlobalSync() const sync = useGlobalSync()
const layout = useLayout() const layout = useLayout()
const platform = usePlatform() const platform = usePlatform()
const dialog = useDialog()
const navigate = useNavigate() const navigate = useNavigate()
const server = useServer()
const homedir = createMemo(() => sync.data.path.home) const homedir = createMemo(() => sync.data.path.home)
function openProject(directory: string) { function openProject(directory: string) {
@@ -22,32 +28,57 @@ export default function Home() {
} }
async function chooseProject() { async function chooseProject() {
const result = await platform.openDirectoryPickerDialog?.({ function resolve(result: string | string[] | null) {
title: "Open project", if (Array.isArray(result)) {
multiple: true, for (const directory of result) {
}) openProject(directory)
if (Array.isArray(result)) { }
for (const directory of result) { } else if (result) {
openProject(directory) 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 ( return (
<div class="mx-auto mt-55"> <div class="mx-auto mt-55">
<Logo class="w-xl opacity-12" /> <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> <Switch>
<Match when={sync.data.project.length > 0}> <Match when={sync.data.project.length > 0}>
<div class="mt-20 w-full flex flex-col gap-4"> <div class="mt-20 w-full flex flex-col gap-4">
<div class="flex gap-2 items-center justify-between pl-3"> <div class="flex gap-2 items-center justify-between pl-3">
<div class="text-14-medium text-text-strong">Recent projects</div> <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}>
<Button icon="folder-add-left" size="normal" class="pl-2 pr-3" onClick={chooseProject}> Open project
Open project </Button>
</Button>
</Show>
</div> </div>
<ul class="flex flex-col gap-2"> <ul class="flex flex-col gap-2">
<For <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 class="text-12-regular text-text-weak">Get started by opening a local project</div>
</div> </div>
<div /> <div />
<Show when={platform.openDirectoryPickerDialog}> <Button class="px-3" onClick={chooseProject}>
<Button class="px-3" onClick={chooseProject}> Open project
Open project </Button>
</Button>
</Show>
</div> </div>
</Match> </Match>
</Switch> </Switch>
+80 -57
View File
@@ -50,8 +50,10 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme" import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
import { DialogSelectProvider } from "@/components/dialog-select-provider" import { DialogSelectProvider } from "@/components/dialog-select-provider"
import { DialogEditProject } from "@/components/dialog-edit-project" import { DialogEditProject } from "@/components/dialog-edit-project"
import { DialogSelectServer } from "@/components/dialog-select-server"
import { useCommand, type CommandOption } from "@/context/command" import { useCommand, type CommandOption } from "@/context/command"
import { ConstrainDragXAxis } from "@/utils/solid-dnd" import { ConstrainDragXAxis } from "@/utils/solid-dnd"
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
export default function Layout(props: ParentProps) { export default function Layout(props: ParentProps) {
const [store, setStore] = createStore({ const [store, setStore] = createStore({
@@ -161,27 +163,33 @@ export default function Layout(props: ParentProps) {
if (e.details?.type !== "permission.updated") return if (e.details?.type !== "permission.updated") return
const directory = e.name const directory = e.name
const permission = e.details.properties 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}` const sessionKey = `${directory}:${permission.sessionID}`
if (seenSessions.has(sessionKey)) return if (seenSessions.has(sessionKey)) return
seenSessions.add(sessionKey) 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({ const toastId = showToast({
persistent: true, persistent: true,
icon: "checklist", icon: "checklist",
title: "Permission required", title: "Permission required",
description: `${sessionTitle} in ${projectName} needs permission`, description,
actions: [ actions: [
{ {
label: "Go to session", label: "Go to session",
onClick: () => { onClick: () => {
navigate(`/${base64Encode(directory)}/session/${permission.sessionID}`) navigate(href)
}, },
}, },
{ {
@@ -332,23 +340,25 @@ export default function Layout(props: ParentProps) {
keybind: "mod+b", keybind: "mod+b",
onSelect: () => layout.sidebar.toggle(), onSelect: () => layout.sidebar.toggle(),
}, },
...(platform.openDirectoryPickerDialog {
? [ id: "project.open",
{ title: "Open project",
id: "project.open", category: "Project",
title: "Open project", keybind: "mod+o",
category: "Project", onSelect: () => chooseProject(),
keybind: "mod+o", },
onSelect: () => chooseProject(),
},
]
: []),
{ {
id: "provider.connect", id: "provider.connect",
title: "Connect provider", title: "Connect provider",
category: "Provider", category: "Provider",
onSelect: () => connectProvider(), onSelect: () => connectProvider(),
}, },
{
id: "server.switch",
title: "Switch server",
category: "Server",
onSelect: () => openServer(),
},
{ {
id: "session.previous", id: "session.previous",
title: "Previous session", title: "Previous session",
@@ -424,6 +434,10 @@ export default function Layout(props: ParentProps) {
dialog.show(() => <DialogSelectProvider />) dialog.show(() => <DialogSelectProvider />)
} }
function openServer() {
dialog.show(() => <DialogSelectServer />)
}
function navigateToProject(directory: string | undefined) { function navigateToProject(directory: string | undefined) {
if (!directory) return if (!directory) return
const lastSession = store.lastSession[directory] const lastSession = store.lastSession[directory]
@@ -451,17 +465,28 @@ export default function Layout(props: ParentProps) {
} }
async function chooseProject() { async function chooseProject() {
const result = await platform.openDirectoryPickerDialog?.({ function resolve(result: string | string[] | null) {
title: "Open project", if (Array.isArray(result)) {
multiple: true, for (const directory of result) {
}) openProject(directory, false)
if (Array.isArray(result)) { }
for (const directory of result) { navigateToProject(result[0])
openProject(directory, false) } 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),
)
} }
} }
@@ -916,7 +941,7 @@ export default function Layout(props: ParentProps) {
</div> </div>
<div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3"> <div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
<Switch> <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="rounded-md bg-background-stronger shadow-xs-border-base">
<div class="p-3 flex flex-col gap-2"> <div class="p-3 flex flex-col gap-2">
<div class="text-12-medium text-text-strong">Getting started</div> <div class="text-12-medium text-text-strong">Getting started</div>
@@ -935,7 +960,7 @@ export default function Layout(props: ParentProps) {
</Tooltip> </Tooltip>
</div> </div>
</Match> </Match>
<Match when={true}> <Match when={providers.all().length > 0}>
<Tooltip placement="right" value="Connect provider" inactive={expanded()}> <Tooltip placement="right" value="Connect provider" inactive={expanded()}>
<Button <Button
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2" class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
@@ -949,30 +974,28 @@ export default function Layout(props: ParentProps) {
</Tooltip> </Tooltip>
</Match> </Match>
</Switch> </Switch>
<Show when={platform.openDirectoryPickerDialog}> <Tooltip
<Tooltip placement="right"
placement="right" value={
value={ <div class="flex items-center gap-2">
<div class="flex items-center gap-2"> <span>Open project</span>
<span>Open project</span> <Show when={!sidebarProps.mobile}>
<Show when={!sidebarProps.mobile}> <span class="text-icon-base text-12-medium">{command.keybind("project.open")}</span>
<span class="text-icon-base text-12-medium">{command.keybind("project.open")}</span> </Show>
</Show> </div>
</div> }
} inactive={expanded()}
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 <Show when={expanded()}>Open project</Show>
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2" </Button>
variant="ghost" </Tooltip>
size="large"
icon="folder-add-left"
onClick={chooseProject}
>
<Show when={expanded()}>Open project</Show>
</Button>
</Tooltip>
</Show>
<Tooltip placement="right" value="Share feedback" inactive={expanded()}> <Tooltip placement="right" value="Share feedback" inactive={expanded()}>
<Button <Button
as={"a"} as={"a"}
@@ -992,7 +1015,7 @@ export default function Layout(props: ParentProps) {
} }
return ( 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 <Header
navigateToProject={navigateToProject} navigateToProject={navigateToProject}
navigateToSession={navigateToSession} navigateToSession={navigateToSession}
+3 -3
View File
@@ -858,7 +858,7 @@ export default function Page() {
</Tabs.List> </Tabs.List>
</div> </div>
<Show when={diffs().length}> <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"> <div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
<SessionReview <SessionReview
classes={{ classes={{
@@ -884,7 +884,7 @@ export default function Page() {
}, },
) )
return ( return (
<Tabs.Content value={tab} class="select-text mt-3"> <Tabs.Content value={tab} class="mt-3">
<Switch> <Switch>
<Match when={file()}> <Match when={file()}>
{(f) => ( {(f) => (
@@ -896,7 +896,7 @@ export default function Page() {
cacheKey: checksum(f().content?.content ?? ""), cacheKey: checksum(f().content?.content ?? ""),
}} }}
overflow="scroll" overflow="scroll"
class="pb-40" class="select-text pb-40"
/> />
)} )}
</Match> </Match>
+1 -1
View File
@@ -10,6 +10,6 @@ export default defineConfig({
}, },
build: { build: {
target: "esnext", target: "esnext",
sourcemap: true, // sourcemap: true,
}, },
}) })
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.0.208", "version": "1.0.217",
"type": "module", "type": "module",
"scripts": { "scripts": {
"typecheck": "tsgo --noEmit", "typecheck": "tsgo --noEmit",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.0.208", "version": "1.0.217",
"private": true, "private": true,
"type": "module", "type": "module",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.0.208", "version": "1.0.217",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.0.208", "version": "1.0.217",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
+2 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"private": true, "private": true,
"version": "1.0.208", "version": "1.0.217",
"type": "module", "type": "module",
"scripts": { "scripts": {
"typecheck": "tsgo -b", "typecheck": "tsgo -b",
@@ -18,6 +18,7 @@
"@tauri-apps/plugin-dialog": "~2", "@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-os": "~2", "@tauri-apps/plugin-os": "~2",
"@tauri-apps/plugin-notification": "~2",
"@tauri-apps/plugin-process": "~2", "@tauri-apps/plugin-process": "~2",
"@tauri-apps/plugin-shell": "~2", "@tauri-apps/plugin-shell": "~2",
"@tauri-apps/plugin-store": "~2", "@tauri-apps/plugin-store": "~2",
+58
View File
@@ -2210,6 +2210,18 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" 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]] [[package]]
name = "markup5ever" name = "markup5ever"
version = "0.14.1" version = "0.14.1"
@@ -2384,6 +2396,20 @@ dependencies = [
"memchr", "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]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.1.0" version = "0.1.0"
@@ -2758,6 +2784,7 @@ dependencies = [
"tauri-plugin-clipboard-manager", "tauri-plugin-clipboard-manager",
"tauri-plugin-dialog", "tauri-plugin-dialog",
"tauri-plugin-http", "tauri-plugin-http",
"tauri-plugin-notification",
"tauri-plugin-opener", "tauri-plugin-opener",
"tauri-plugin-os", "tauri-plugin-os",
"tauri-plugin-process", "tauri-plugin-process",
@@ -4519,6 +4546,25 @@ dependencies = [
"urlpattern", "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]] [[package]]
name = "tauri-plugin-opener" name = "tauri-plugin-opener"
version = "2.5.2" version = "2.5.2"
@@ -4754,6 +4800,18 @@ dependencies = [
"toml 0.9.8", "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]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.23.0" version = "3.23.0"
+1
View File
@@ -28,6 +28,7 @@ tauri-plugin-store = "2"
tauri-plugin-window-state = "2" tauri-plugin-window-state = "2"
tauri-plugin-clipboard-manager = "2" tauri-plugin-clipboard-manager = "2"
tauri-plugin-http = "2" tauri-plugin-http = "2"
tauri-plugin-notification = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
@@ -8,6 +8,10 @@
"opener:default", "opener:default",
"core:window:allow-start-dragging", "core:window:allow-start-dragging",
"core:webview:allow-set-webview-zoom", "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", "shell:default",
"updater:default", "updater:default",
"dialog:default", "dialog:default",
@@ -15,6 +19,7 @@
"store:default", "store:default",
"window-state:default", "window-state:default",
"os:default", "os:default",
"notification:default",
{ {
"identifier": "http:default", "identifier": "http:default",
"allow": [{ "url": "http://*" }, { "url": "https://*" }, { "url": "http://*:*/*" }] "allow": [{ "url": "http://*" }, { "url": "https://*" }, { "url": "http://*:*/*" }]
+1
View File
@@ -198,6 +198,7 @@ pub fn run() {
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_notification::init())
.plugin(PinchZoomDisablePlugin) .plugin(PinchZoomDisablePlugin)
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
kill_sidecar, kill_sidecar,
+29
View File
@@ -12,6 +12,8 @@ import { UPDATER_ENABLED } from "./updater"
import { createMenu } from "./menu" import { createMenu } from "./menu"
import { check, Update } from "@tauri-apps/plugin-updater" import { check, Update } from "@tauri-apps/plugin-updater"
import { invoke } from "@tauri-apps/api/core" 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 { relaunch } from "@tauri-apps/plugin-process"
import pkg from "../package.json" import pkg from "../package.json"
@@ -94,6 +96,33 @@ const platform: Platform = {
await relaunch() 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 // @ts-expect-error
fetch: tauriFetch, fetch: tauriFetch,
} }
+6 -2
View File
@@ -10,9 +10,13 @@ export default defineConfig({
// //
// 1. prevent Vite from obscuring rust errors // 1. prevent Vite from obscuring rust errors
clearScreen: false, clearScreen: false,
build: { esbuild: {
sourcemap: true, // Improves production stack traces
keepNames: true,
}, },
// build: {
// sourcemap: true,
// },
// 2. tauri expects a fixed port, fail if that port is not available // 2. tauri expects a fixed port, fail if that port is not available
server: { server: {
port: 1420, port: 1420,
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.0.208", "version": "1.0.217",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+8 -5
View File
@@ -3,6 +3,7 @@ import { FileRoutes } from "@solidjs/start/router"
import { Font } from "@opencode-ai/ui/font" import { Font } from "@opencode-ai/ui/font"
import { MetaProvider } from "@solidjs/meta" import { MetaProvider } from "@solidjs/meta"
import { MarkedProvider } from "@opencode-ai/ui/context/marked" import { MarkedProvider } from "@opencode-ai/ui/context/marked"
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
import { Suspense } from "solid-js" import { Suspense } from "solid-js"
import "./app.css" import "./app.css"
import { Favicon } from "@opencode-ai/ui/favicon" import { Favicon } from "@opencode-ai/ui/favicon"
@@ -12,11 +13,13 @@ export default function App() {
<Router <Router
root={(props) => ( root={(props) => (
<MetaProvider> <MetaProvider>
<MarkedProvider> <DialogProvider>
<Favicon /> <MarkedProvider>
<Font /> <Favicon />
<Suspense>{props.children}</Suspense> <Font />
</MarkedProvider> <Suspense>{props.children}</Suspense>
</MarkedProvider>
</DialogProvider>
</MetaProvider> </MetaProvider>
)} )}
> >
@@ -162,11 +162,20 @@ export default function () {
return ( return (
<ErrorBoundary <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 ( return (
<Show when={e.message === "SessionDataMissingError"}> <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">
<NotFound /> <p class="text-16-medium">Unable to render this share.</p>
</Show> <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>
) )
}} }}
> >
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode" id = "opencode"
name = "OpenCode" name = "OpenCode"
description = "The open source coding agent." description = "The open source coding agent."
version = "1.0.208" version = "1.0.217"
schema_version = 1 schema_version = 1
authors = ["Anomaly"] authors = ["Anomaly"]
repository = "https://github.com/sst/opencode" repository = "https://github.com/sst/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg" icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64] [agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.208/opencode-darwin-arm64.zip" archive = "https://github.com/sst/opencode/releases/download/v1.0.217/opencode-darwin-arm64.zip"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64] [agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.208/opencode-darwin-x64.zip" archive = "https://github.com/sst/opencode/releases/download/v1.0.217/opencode-darwin-x64.zip"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64] [agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.208/opencode-linux-arm64.tar.gz" archive = "https://github.com/sst/opencode/releases/download/v1.0.217/opencode-linux-arm64.tar.gz"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64] [agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.208/opencode-linux-x64.tar.gz" archive = "https://github.com/sst/opencode/releases/download/v1.0.217/opencode-linux-x64.tar.gz"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64] [agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.208/opencode-windows-x64.zip" archive = "https://github.com/sst/opencode/releases/download/v1.0.217/opencode-windows-x64.zip"
cmd = "./opencode.exe" cmd = "./opencode.exe"
args = ["acp"] args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.0.208", "version": "1.0.217",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",
+1
View File
@@ -2,5 +2,6 @@ preload = ["@opentui/solid/preload"]
[test] [test]
preload = ["./test/preload.ts"] preload = ["./test/preload.ts"]
timeout = 10000 # 10 seconds (default is 5000ms)
# Enable code coverage # Enable code coverage
coverage = true coverage = true
+6 -7
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.0.208", "version": "1.0.217",
"name": "opencode", "name": "opencode",
"type": "module", "type": "module",
"private": true, "private": true,
@@ -51,18 +51,17 @@
"@agentclientprotocol/sdk": "0.5.1", "@agentclientprotocol/sdk": "0.5.1",
"@ai-sdk/amazon-bedrock": "3.0.57", "@ai-sdk/amazon-bedrock": "3.0.57",
"@ai-sdk/anthropic": "2.0.56", "@ai-sdk/anthropic": "2.0.56",
"@ai-sdk/azure": "2.0.73", "@ai-sdk/azure": "2.0.82",
"@ai-sdk/cerebras": "1.0.33", "@ai-sdk/cerebras": "1.0.33",
"@ai-sdk/cohere": "2.0.21", "@ai-sdk/cohere": "2.0.21",
"@ai-sdk/deepinfra": "1.0.30", "@ai-sdk/deepinfra": "1.0.30",
"@ai-sdk/gateway": "2.0.23", "@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/google-vertex": "3.0.81",
"@ai-sdk/groq": "2.0.33", "@ai-sdk/groq": "2.0.33",
"@ai-sdk/mcp": "0.0.8",
"@ai-sdk/mistral": "2.0.26", "@ai-sdk/mistral": "2.0.26",
"@ai-sdk/openai": "2.0.71", "@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/perplexity": "2.0.22",
"@ai-sdk/provider": "2.0.0", "@ai-sdk/provider": "2.0.0",
"@ai-sdk/provider-utils": "3.0.19", "@ai-sdk/provider-utils": "3.0.19",
@@ -80,8 +79,8 @@
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*", "@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "1.5.2", "@openrouter/ai-sdk-provider": "1.5.2",
"@opentui/core": "0.1.63", "@opentui/core": "0.1.67",
"@opentui/solid": "0.1.63", "@opentui/solid": "0.1.67",
"@parcel/watcher": "2.5.1", "@parcel/watcher": "2.5.1",
"@pierre/diffs": "catalog:", "@pierre/diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2", "@solid-primitives/event-bus": "1.1.2",
+14
View File
@@ -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",
],
},
},
], ],
} }
+6
View File
@@ -349,6 +349,12 @@ export const AuthLoginCommand = cmd({
prompts.log.info("You can create an api key at https://vercel.link/ai-gateway-token") 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({ const key = await prompts.password({
message: "Enter your API key", message: "Enter your API key",
validate: (x) => (x && x.length > 0 ? undefined : "Required"), validate: (x) => (x && x.length > 0 ? undefined : "Required"),
@@ -372,6 +372,15 @@ function App() {
local.agent.move(1) local.agent.move(1)
}, },
}, },
{
title: "Variant cycle",
value: "variant.cycle",
keybind: "variant_cycle",
category: "Agent",
onSelect: () => {
local.model.variant.cycle()
},
},
{ {
title: "Agent cycle reverse", title: "Agent cycle reverse",
value: "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, (item) => item.providerID === value.providerID && item.modelID === value.modelID,
) )
if (inFavorites) return false if (inFavorites) return false
const inRecents = recents.some( const inRecents = recentList.some(
(item) => item.providerID === value.providerID && item.modelID === value.modelID, (item) => item.providerID === value.providerID && item.modelID === value.modelID,
) )
if (inRecents) return false if (inRecents) return false
@@ -167,6 +167,13 @@ export function Prompt(props: PromptProps) {
if (!props.disabled) input.cursorColor = theme.text 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<{ const [store, setStore] = createStore<{
prompt: PromptInfo prompt: PromptInfo
mode: "normal" | "shell" mode: "normal" | "shell"
@@ -184,6 +191,23 @@ export function Prompt(props: PromptProps) {
interrupt: 0, 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(() => { command.register(() => {
return [ return [
{ {
@@ -562,6 +586,7 @@ export function Prompt(props: PromptProps) {
// Capture mode before it gets reset // Capture mode before it gets reset
const currentMode = store.mode const currentMode = store.mode
const variant = local.model.variant.current()
if (store.mode === "shell") { if (store.mode === "shell") {
sdk.client.session.shell({ sdk.client.session.shell({
@@ -590,6 +615,7 @@ export function Prompt(props: PromptProps) {
agent: local.agent.current().name, agent: local.agent.current().name,
model: `${selectedModel.providerID}/${selectedModel.modelID}`, model: `${selectedModel.providerID}/${selectedModel.modelID}`,
messageID, messageID,
variant,
}) })
} else { } else {
sdk.client.session.prompt({ sdk.client.session.prompt({
@@ -598,6 +624,7 @@ export function Prompt(props: PromptProps) {
messageID, messageID,
agent: local.agent.current().name, agent: local.agent.current().name,
model: selectedModel, model: selectedModel,
variant,
parts: [ parts: [
{ {
id: Identifier.ascending("part"), id: Identifier.ascending("part"),
@@ -718,6 +745,13 @@ export function Prompt(props: PromptProps) {
return local.agent.color(local.agent.current().name) 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 spinnerDef = createMemo(() => {
const color = local.agent.color(local.agent.current().name) const color = local.agent.color(local.agent.current().name)
return { return {
@@ -958,6 +992,12 @@ export function Prompt(props: PromptProps) {
{local.model.parsed().model} {local.model.parsed().model}
</text> </text>
<text fg={theme.textMuted}>{local.model.parsed().provider}</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> </box>
</Show> </Show>
</box> </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 agent = iife(() => {
const agents = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden)) const agents = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden))
const [agentStore, setAgentStore] = createStore<{ const [agentStore, setAgentStore] = createStore<{
@@ -120,11 +102,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
providerID: string providerID: string
modelID: string modelID: string
}[] }[]
variant: Record<string, string | undefined>
}>({ }>({
ready: false, ready: false,
model: {}, model: {},
recent: [], recent: [],
favorite: [], favorite: [],
variant: {},
}) })
const file = Bun.file(path.join(Global.Path.state, "model.json")) const file = Bun.file(path.join(Global.Path.state, "model.json"))
@@ -135,6 +119,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
JSON.stringify({ JSON.stringify({
recent: modelStore.recent, recent: modelStore.recent,
favorite: modelStore.favorite, favorite: modelStore.favorite,
variant: modelStore.variant,
}), }),
) )
} }
@@ -144,6 +129,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
.then((x) => { .then((x) => {
if (Array.isArray(x.recent)) setModelStore("recent", x.recent) if (Array.isArray(x.recent)) setModelStore("recent", x.recent)
if (Array.isArray(x.favorite)) setModelStore("favorite", x.favorite) if (Array.isArray(x.favorite)) setModelStore("favorite", x.favorite)
if (typeof x.variant === "object" && x.variant !== null) setModelStore("variant", x.variant)
}) })
.catch(() => {}) .catch(() => {})
.finally(() => { .finally(() => {
@@ -218,6 +204,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
return { return {
provider: "Connect a provider", provider: "Connect a provider",
model: "No provider selected", model: "No provider selected",
reasoning: false,
} }
} }
const provider = sync.data.provider.find((x) => x.id === value.providerID) const provider = sync.data.provider.find((x) => x.id === value.providerID)
@@ -225,6 +212,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
return { return {
provider: provider?.name ?? value.providerID, provider: provider?.name ?? value.providerID,
model: info?.name ?? value.modelID, model: info?.name ?? value.modelID,
reasoning: info?.capabilities?.reasoning ?? false,
} }
}), }),
cycle(direction: 1 | -1) { cycle(direction: 1 | -1) {
@@ -267,7 +255,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
setModelStore("model", agent.current().name, { ...next }) setModelStore("model", agent.current().name, { ...next })
const uniq = uniqueBy([next, ...modelStore.recent], (x) => x.providerID + x.modelID) const uniq = uniqueBy([next, ...modelStore.recent], (x) => x.providerID + x.modelID)
if (uniq.length > 10) uniq.pop() if (uniq.length > 10) uniq.pop()
setModelStore("recent", uniq) setModelStore(
"recent",
uniq.map((x) => ({ providerID: x.providerID, modelID: x.modelID })),
)
save() save()
}, },
set(model: { providerID: string; modelID: string }, options?: { recent?: boolean }) { set(model: { providerID: string; modelID: string }, options?: { recent?: boolean }) {
@@ -284,7 +275,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
if (options?.recent) { if (options?.recent) {
const uniq = uniqueBy([model, ...modelStore.recent], (x) => x.providerID + x.modelID) const uniq = uniqueBy([model, ...modelStore.recent], (x) => x.providerID + x.modelID)
if (uniq.length > 10) uniq.pop() if (uniq.length > 10) uniq.pop()
setModelStore("recent", uniq) setModelStore(
"recent",
uniq.map((x) => ({ providerID: x.providerID, modelID: x.modelID })),
)
save() save()
} }
}) })
@@ -305,10 +299,53 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const next = exists const next = exists
? modelStore.favorite.filter((x) => x.providerID !== model.providerID || x.modelID !== model.modelID) ? modelStore.favorite.filter((x) => x.providerID !== model.providerID || x.modelID !== model.modelID)
: [model, ...modelStore.favorite] : [model, ...modelStore.favorite]
setModelStore("favorite", next) setModelStore(
"favorite",
next.map((x) => ({ providerID: x.providerID, modelID: x.modelID })),
)
save() 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 = { const result = {
model, model,
agent, agent,
@@ -115,11 +115,12 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
setStore("selected", currentIndex) setStore("selected", currentIndex)
} }
} }
scroll.scrollTo(0) scroll?.scrollTo(0)
}), }),
) )
function move(direction: number) { function move(direction: number) {
if (flat().length === 0) return
let next = store.selected + direction let next = store.selected + direction
if (next < 0) next = flat().length - 1 if (next < 0) next = flat().length - 1
if (next >= flat().length) next = 0 if (next >= flat().length) next = 0
@@ -129,6 +130,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
function moveTo(next: number) { function moveTo(next: number) {
setStore("selected", next) setStore("selected", next)
props.onMove?.(selected()!) props.onMove?.(selected()!)
if (!scroll) return
const target = scroll.getChildren().find((child) => { const target = scroll.getChildren().find((child) => {
return child.id === JSON.stringify(selected()?.value) 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> = { const ref: DialogSelectRef<T> = {
get filter() { get filter() {
return store.filter return store.filter
@@ -213,61 +215,70 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
/> />
</box> </box>
</box> </box>
<scrollbox <Show
paddingLeft={1} when={grouped().length > 0}
paddingRight={1} fallback={
scrollbarOptions={{ visible: false }} <box paddingLeft={4} paddingRight={4} paddingTop={1}>
ref={(r: ScrollBoxRenderable) => (scroll = r)} <text fg={theme.textMuted}>No results found</text>
maxHeight={height()} </box>
}
> >
<For each={grouped()}> <scrollbox
{([category, options], index) => ( paddingLeft={1}
<> paddingRight={1}
<Show when={category}> scrollbarOptions={{ visible: false }}
<box paddingTop={index() > 0 ? 1 : 0} paddingLeft={3}> ref={(r: ScrollBoxRenderable) => (scroll = r)}
<text fg={theme.accent} attributes={TextAttributes.BOLD}> maxHeight={height()}
{category} >
</text> <For each={grouped()}>
</box> {([category, options], index) => (
</Show> <>
<For each={options}> <Show when={category}>
{(option) => { <box paddingTop={index() > 0 ? 1 : 0} paddingLeft={3}>
const active = createMemo(() => isDeepEqual(option.value, selected()?.value)) <text fg={theme.accent} attributes={TextAttributes.BOLD}>
const current = createMemo(() => isDeepEqual(option.value, props.current)) {category}
return ( </text>
<box </box>
id={JSON.stringify(option.value)} </Show>
flexDirection="row" <For each={options}>
onMouseUp={() => { {(option) => {
option.onSelect?.(dialog) const active = createMemo(() => isDeepEqual(option.value, selected()?.value))
props.onSelect?.(option) const current = createMemo(() => isDeepEqual(option.value, props.current))
}} return (
onMouseOver={() => { <box
const index = filtered().findIndex((x) => isDeepEqual(x.value, option.value)) id={JSON.stringify(option.value)}
if (index === -1) return flexDirection="row"
moveTo(index) onMouseUp={() => {
}} option.onSelect?.(dialog)
backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)} props.onSelect?.(option)
paddingLeft={current() || option.gutter ? 1 : 3} }}
paddingRight={3} onMouseOver={() => {
gap={1} const index = filtered().findIndex((x) => isDeepEqual(x.value, option.value))
> if (index === -1) return
<Option moveTo(index)
title={option.title} }}
footer={option.footer} backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)}
description={option.description !== category ? option.description : undefined} paddingLeft={current() || option.gutter ? 1 : 3}
active={active()} paddingRight={3}
current={current()} gap={1}
gutter={option.gutter} >
/> <Option
</box> title={option.title}
) footer={option.footer}
}} description={option.description !== category ? option.description : undefined}
</For> active={active()}
</> current={current()}
)} gutter={option.gutter}
</For> />
</scrollbox> </box>
)
}}
</For>
</>
)}
</For>
</scrollbox>
</Show>
<Show when={keybinds().length} fallback={<box flexShrink={0} />}> <Show when={keybinds().length} fallback={<box flexShrink={0} />}>
<box paddingRight={2} paddingLeft={4} flexDirection="row" gap={2} flexShrink={0} paddingTop={1}> <box paddingRight={2} paddingLeft={4} flexDirection="row" gap={2} flexShrink={0} paddingTop={1}>
<For each={keybinds()}> <For each={keybinds()}>
+1
View File
@@ -490,6 +490,7 @@ export namespace Config {
agent_list: z.string().optional().default("<leader>a").describe("List agents"), agent_list: z.string().optional().default("<leader>a").describe("List agents"),
agent_cycle: z.string().optional().default("tab").describe("Next agent"), agent_cycle: z.string().optional().default("tab").describe("Next agent"),
agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous 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_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"), input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
input_submit: z.string().optional().default("return").describe("Submit input"), input_submit: z.string().optional().default("return").describe("Submit input"),
+76 -8
View File
@@ -11,6 +11,7 @@ import { Filesystem } from "../util/filesystem"
import { Instance } from "../project/instance" import { Instance } from "../project/instance"
import { Ripgrep } from "./ripgrep" import { Ripgrep } from "./ripgrep"
import fuzzysort from "fuzzysort" import fuzzysort from "fuzzysort"
import { Global } from "../global"
export namespace File { export namespace File {
const log = Log.create({ service: "file" }) const log = Log.create({ service: "file" })
@@ -122,10 +123,49 @@ export namespace File {
type Entry = { files: string[]; dirs: string[] } type Entry = { files: string[]; dirs: string[] }
let cache: Entry = { files: [], dirs: [] } let cache: Entry = { files: [], dirs: [] }
let fetching = false let fetching = false
const isGlobalHome = Instance.directory === Global.Path.home && Instance.project.id === "global"
const fn = async (result: Entry) => { const fn = async (result: Entry) => {
// Disable scanning if in root of file system // Disable scanning if in root of file system
if (Instance.directory === path.parse(Instance.directory).root) return if (Instance.directory === path.parse(Instance.directory).root) return
fetching = true 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>() const set = new Set<string>()
for await (const file of Ripgrep.files({ cwd: Instance.directory })) { for await (const file of Ripgrep.files({ cwd: Instance.directory })) {
result.files.push(file) result.files.push(file)
@@ -329,15 +369,43 @@ export namespace File {
}) })
} }
export async function search(input: { query: string; limit?: number; dirs?: boolean }) { export async function search(input: { query: string; limit?: number; dirs?: boolean; type?: "file" | "directory" }) {
log.info("search", { query: input.query }) const query = input.query.trim()
const limit = input.limit ?? 100 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()) 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 hidden = (item: string) => {
const items = input.dirs !== false ? [...result.files, ...result.dirs] : result.files const normalized = item.replaceAll("\\", "/").replace(/\/+$/, "")
const sorted = fuzzysort.go(input.query, items, { limit: limit }).map((r) => r.target) return normalized.split("/").some((p) => p.startsWith(".") && p.length > 1)
log.info("search", { query: input.query, results: sorted.length }) }
return sorted 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
} }
} }
+11 -2
View File
@@ -205,8 +205,17 @@ export namespace Ripgrep {
return filepath return filepath
} }
export async function* files(input: { cwd: string; glob?: string[] }) { export async function* files(input: {
const args = [await filepath(), "--files", "--follow", "--hidden", "--glob=!.git/*"] 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) { if (input.glob) {
for (const g of input.glob) { for (const g of input.glob) {
args.push(`--glob=${g}`) args.push(`--glob=${g}`)
+26 -12
View File
@@ -9,9 +9,13 @@ import path from "path"
// @ts-ignore // @ts-ignore
import { createWrapper } from "@parcel/watcher/wrapper" import { createWrapper } from "@parcel/watcher/wrapper"
import { lazy } from "@/util/lazy" import { lazy } from "@/util/lazy"
import { withTimeout } from "@/util/timeout"
import type ParcelWatcher from "@parcel/watcher" import type ParcelWatcher from "@parcel/watcher"
import { $ } from "bun" import { $ } from "bun"
import { Flag } from "@/flag/flag" import { Flag } from "@/flag/flag"
import { readdir } from "fs/promises"
const SUBSCRIBE_TIMEOUT_MS = 10_000
declare const OPENCODE_LIBC: string | undefined declare const OPENCODE_LIBC: string | undefined
@@ -63,12 +67,16 @@ export namespace FileWatcher {
const cfgIgnores = cfg.watcher?.ignore ?? [] const cfgIgnores = cfg.watcher?.ignore ?? []
if (Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER) { if (Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER) {
subs.push( const pending = watcher().subscribe(Instance.directory, subscribe, {
await watcher().subscribe(Instance.directory, subscribe, { ignore: [...FileIgnore.PATTERNS, ...cfgIgnores],
ignore: [...FileIgnore.PATTERNS, ...cfgIgnores], backend,
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` const vcsDir = await $`git rev-parse --git-dir`
@@ -78,12 +86,18 @@ export namespace FileWatcher {
.text() .text()
.then((x) => path.resolve(Instance.worktree, x.trim())) .then((x) => path.resolve(Instance.worktree, x.trim()))
if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) { if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) {
subs.push( const gitDirContents = await readdir(vcsDir).catch(() => [])
await watcher().subscribe(vcsDir, subscribe, { const ignoreList = gitDirContents.filter((entry) => entry !== "HEAD")
ignore: ["hooks", "info", "logs", "objects", "refs", "worktrees", "modules", "lfs"], const pending = watcher().subscribe(vcsDir, subscribe, {
backend, 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 } return { subs }
@@ -322,3 +322,12 @@ export const shfmt: Info = {
return Bun.which("shfmt") !== null return Bun.which("shfmt") !== null
}, },
} }
export const nixfmt: Info = {
name: "nixfmt",
command: ["nixfmt", "$FILE"],
extensions: [".nix"],
async enabled() {
return Bun.which("nixfmt") !== null
},
}
+5 -2
View File
@@ -12,14 +12,17 @@ const state = path.join(xdgState!, app)
export namespace Global { export namespace Global {
export const Path = { 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, data,
bin: path.join(data, "bin"), bin: path.join(data, "bin"),
log: path.join(data, "log"), log: path.join(data, "log"),
cache, cache,
config, config,
state, state,
} as const }
} }
await Promise.all([ await Promise.all([
+1
View File
@@ -24,6 +24,7 @@ export namespace Plugin {
project: Instance.project, project: Instance.project,
worktree: Instance.worktree, worktree: Instance.worktree,
directory: Instance.directory, directory: Instance.directory,
serverUrl: Server.url(),
$: Bun.$, $: Bun.$,
} }
const plugins = [...(config.plugin ?? [])] const plugins = [...(config.plugin ?? [])]
+18 -1
View File
@@ -34,6 +34,7 @@ import { createCohere } from "@ai-sdk/cohere"
import { createGateway } from "@ai-sdk/gateway" import { createGateway } from "@ai-sdk/gateway"
import { createTogetherAI } from "@ai-sdk/togetherai" import { createTogetherAI } from "@ai-sdk/togetherai"
import { createPerplexity } from "@ai-sdk/perplexity" import { createPerplexity } from "@ai-sdk/perplexity"
import { ProviderTransform } from "./transform"
export namespace Provider { export namespace Provider {
const log = Log.create({ service: "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 export const Model = z
.object({ .object({
id: z.string(), id: z.string(),
@@ -467,6 +478,7 @@ export namespace Provider {
options: z.record(z.string(), z.any()), options: z.record(z.string(), z.any()),
headers: z.record(z.string(), z.string()), headers: z.record(z.string(), z.string()),
release_date: z.string(), release_date: z.string(),
variants: z.record(z.string(), Variant).optional(),
}) })
.meta({ .meta({
ref: "Model", ref: "Model",
@@ -489,7 +501,7 @@ export namespace Provider {
export type Info = z.infer<typeof Info> export type Info = z.infer<typeof Info>
function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model { function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
return { const m: Model = {
id: model.id, id: model.id,
providerID: provider.id, providerID: provider.id,
name: model.name, name: model.name,
@@ -546,7 +558,12 @@ export namespace Provider {
interleaved: model.interleaved ?? false, interleaved: model.interleaved ?? false,
}, },
release_date: model.release_date, 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 { export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
+169 -11
View File
@@ -124,7 +124,7 @@ export namespace ProviderTransform {
cacheControl: { type: "ephemeral" }, cacheControl: { type: "ephemeral" },
}, },
openrouter: { openrouter: {
cache_control: { type: "ephemeral" }, cacheControl: { type: "ephemeral" },
}, },
bedrock: { bedrock: {
cachePoint: { type: "ephemeral" }, cachePoint: { type: "ephemeral" },
@@ -243,6 +243,162 @@ export namespace ProviderTransform {
return undefined 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( export function options(
model: Provider.Model, model: Provider.Model,
sessionID: string, sessionID: string,
@@ -302,26 +458,27 @@ export namespace ProviderTransform {
} }
export function smallOptions(model: Provider.Model) { export function smallOptions(model: Provider.Model) {
const options: Record<string, any> = {}
if (model.providerID === "openai" || model.api.id.includes("gpt-5")) { if (model.providerID === "openai" || model.api.id.includes("gpt-5")) {
if (model.api.id.includes("5.")) { if (model.api.id.includes("5.")) {
options["reasoningEffort"] = "low" return { reasoningEffort: "low" }
} else {
options["reasoningEffort"] = "minimal"
} }
return { reasoningEffort: "minimal" }
} }
if (model.providerID === "google") { if (model.providerID === "google") {
options["thinkingConfig"] = { return { thinkingConfig: { thinkingBudget: 0 } }
thinkingBudget: 0,
}
} }
if (model.providerID === "openrouter") {
return options if (model.api.id.includes("google")) {
return { reasoning: { enabled: false } }
}
return { reasoningEffort: "minimal" }
}
return {}
} }
export function providerOptions(model: Provider.Model, options: { [x: string]: any }) { export function providerOptions(model: Provider.Model, options: { [x: string]: any }) {
switch (model.api.npm) { switch (model.api.npm) {
case "@ai-sdk/github-copilot":
case "@ai-sdk/openai": case "@ai-sdk/openai":
case "@ai-sdk/azure": case "@ai-sdk/azure":
return { return {
@@ -335,6 +492,7 @@ export namespace ProviderTransform {
return { return {
["anthropic" as string]: options, ["anthropic" as string]: options,
} }
case "@ai-sdk/google-vertex":
case "@ai-sdk/google": case "@ai-sdk/google":
return { return {
["google" as string]: options, ["google" as string]: options,
+1 -1
View File
@@ -1,5 +1,5 @@
import { Log } from "@/util/log" import { Log } from "@/util/log"
import Bonjour from "bonjour-service" import { Bonjour } from "bonjour-service"
const log = Log.create({ service: "mdns" }) const log = Log.create({ service: "mdns" })
+32 -3
View File
@@ -57,6 +57,12 @@ globalThis.AI_SDK_LOG_WARNINGS = false
export namespace Server { export namespace Server {
const log = Log.create({ service: "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 = { export const Event = {
Connected: BusEvent.define("server.connected", z.object({})), Connected: BusEvent.define("server.connected", z.object({})),
Disposed: BusEvent.define("global.disposed", z.object({})), Disposed: BusEvent.define("global.disposed", z.object({})),
@@ -98,7 +104,23 @@ export namespace Server {
timer.stop() 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( .get(
"/global/health", "/global/health",
describeRoute({ describeRoute({
@@ -1795,7 +1817,7 @@ export namespace Server {
"/find/file", "/find/file",
describeRoute({ describeRoute({
summary: "Find files", 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", operationId: "find.files",
responses: { responses: {
200: { 200: {
@@ -1813,15 +1835,20 @@ export namespace Server {
z.object({ z.object({
query: z.string(), query: z.string(),
dirs: z.enum(["true", "false"]).optional(), dirs: z.enum(["true", "false"]).optional(),
type: z.enum(["file", "directory"]).optional(),
limit: z.coerce.number().int().min(1).max(200).optional(),
}), }),
), ),
async (c) => { async (c) => {
const query = c.req.valid("query").query const query = c.req.valid("query").query
const dirs = c.req.valid("query").dirs 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({ const results = await File.search({
query, query,
limit: 10, limit: limit ?? 10,
dirs: dirs !== "false", dirs: dirs !== "false",
type,
}) })
return c.json(results) return c.json(results)
}, },
@@ -2666,6 +2693,8 @@ export namespace Server {
const server = opts.port === 0 ? (tryServe(4096) ?? tryServe(0)) : tryServe(opts.port) 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}`) if (!server) throw new Error(`Failed to start server on port ${opts.port}`)
_url = server.url
const shouldPublishMDNS = const shouldPublishMDNS =
opts.mdns && opts.mdns &&
server.port && server.port &&
+9 -7
View File
@@ -74,6 +74,14 @@ export namespace LLM {
} }
const provider = await Provider.getProvider(input.model.providerID) 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( const params = await Plugin.trigger(
"chat.params", "chat.params",
@@ -90,13 +98,7 @@ export namespace LLM {
: undefined, : undefined,
topP: input.agent.topP ?? ProviderTransform.topP(input.model), topP: input.agent.topP ?? ProviderTransform.topP(input.model),
topK: ProviderTransform.topK(input.model), topK: ProviderTransform.topK(input.model),
options: pipe( options,
{},
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),
),
}, },
) )
+2 -3
View File
@@ -1,8 +1,6 @@
import { BusEvent } from "@/bus/bus-event" import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import z from "zod" import z from "zod"
import { NamedError } from "@opencode-ai/util/error" import { NamedError } from "@opencode-ai/util/error"
import { Message } from "./message"
import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai" import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai"
import { Identifier } from "../id/id" import { Identifier } from "../id/id"
import { LSP } from "../lsp" import { LSP } from "../lsp"
@@ -308,6 +306,7 @@ export namespace MessageV2 {
}), }),
system: z.string().optional(), system: z.string().optional(),
tools: z.record(z.string(), z.boolean()).optional(), tools: z.record(z.string(), z.boolean()).optional(),
variant: z.string().optional(),
}).meta({ }).meta({
ref: "UserMessage", 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) { export const stream = fn(Identifier.schema("session"), async function* (sessionID) {
+4
View File
@@ -90,6 +90,7 @@ export namespace SessionPrompt {
noReply: z.boolean().optional(), noReply: z.boolean().optional(),
tools: z.record(z.string(), z.boolean()).optional(), tools: z.record(z.string(), z.boolean()).optional(),
system: z.string().optional(), system: z.string().optional(),
variant: z.string().optional(),
parts: z.array( parts: z.array(
z.discriminatedUnion("type", [ z.discriminatedUnion("type", [
MessageV2.TextPart.omit({ MessageV2.TextPart.omit({
@@ -727,6 +728,7 @@ export namespace SessionPrompt {
agent: agent.name, agent: agent.name,
model: input.model ?? agent.model ?? (await lastModel(input.sessionID)), model: input.model ?? agent.model ?? (await lastModel(input.sessionID)),
system: input.system, system: input.system,
variant: input.variant,
} }
const parts = await Promise.all( const parts = await Promise.all(
@@ -1267,6 +1269,7 @@ export namespace SessionPrompt {
model: z.string().optional(), model: z.string().optional(),
arguments: z.string(), arguments: z.string(),
command: z.string(), command: z.string(),
variant: z.string().optional(),
}) })
export type CommandInput = z.infer<typeof CommandInput> export type CommandInput = z.infer<typeof CommandInput>
const bashRegex = /!`([^`]+)`/g const bashRegex = /!`([^`]+)`/g
@@ -1369,6 +1372,7 @@ export namespace SessionPrompt {
model, model,
agent: agentName, agent: agentName,
parts, parts,
variant: input.variant,
})) as MessageV2.WithParts })) as MessageV2.WithParts
Bus.publish(Command.Event.Executed, { 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 - Choose descriptive names that fit the content
- Keep headers short (13 words) and in `**Title Case**`. Always start headers with `**` and end with `**` - Keep headers short (13 words) and in `**Title Case**`. Always start headers with `**` and end with `**`
- Leave no blank line before the first bullet under a header. - 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** **Bullets**
@@ -289,7 +289,7 @@ When referencing files in your response, make sure to include the relevant start
- Dont nest bullets or create deep hierarchies. - Dont nest bullets or create deep hierarchies.
- Dont output ANSI escape codes directly — the CLI renderer applies them. - Dont output ANSI escape codes directly — the CLI renderer applies them.
- Dont cram unrelated keywords into a single bullet; split for clarity. - Dont cram unrelated keywords into a single bullet; split for clarity.
- Dont let keyword lists run long — wrap or reformat for scanability. - Dont 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 whats 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. 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 whats 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 ## 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. 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 ## 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> </system-reminder>
+32 -13
View File
@@ -4,6 +4,9 @@ import { Instance } from "../project/instance"
import { NamedError } from "@opencode-ai/util/error" import { NamedError } from "@opencode-ai/util/error"
import { ConfigMarkdown } from "../config/markdown" import { ConfigMarkdown } from "../config/markdown"
import { Log } from "../util/log" import { Log } from "../util/log"
import { Global } from "@/global"
import { Filesystem } from "@/util/filesystem"
import { exists } from "fs/promises"
export namespace Skill { export namespace Skill {
const log = Log.create({ service: "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 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 () => { export const state = Instance.state(async () => {
const directories = await Config.directories()
const skills: Record<string, Info> = {} const skills: Record<string, Info> = {}
const addSkill = async (match: string) => { 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({ for await (const match of OPENCODE_SKILL_GLOB.scan({
cwd: dir, cwd: dir,
absolute: true, 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 return skills
}) })
+1 -1
View File
@@ -280,7 +280,7 @@ export const BashTool = Tool.define("bash", async () => {
} }
if (timedOut) { 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) { if (aborted) {
+1 -1
View File
@@ -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: Available agent types and the tools they have access to:
{agents} {agents}
+1 -1
View File
@@ -4,7 +4,7 @@ It also helps the user understand the progress of the task and overall progress
## When to Use This Tool ## When to Use This Tool
Use this tool proactively in these scenarios: 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 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 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) 4. User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated)
+2 -1
View File
@@ -15,7 +15,8 @@ export const WebFetchTool = Tool.define("webfetch", {
url: z.string().describe("The URL to fetch content from"), url: z.string().describe("The URL to fetch content from"),
format: z format: z
.enum(["text", "markdown", "html"]) .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(), timeout: z.number().describe("Optional timeout in seconds (max 120)").optional(),
}), }),
async execute(params, ctx) { async execute(params, ctx) {
+4 -4
View File
@@ -1,13 +1,13 @@
- Fetches content from a specified URL - Fetches content from a specified URL
- Takes a URL and a prompt as input - Takes a URL and optional format as input
- Fetches the URL content, converts HTML to markdown - Fetches the URL content, converts to requested format (markdown by default)
- Returns the model's response about the content - Returns the content in the specified format
- Use this tool when you need to retrieve and analyze web content - Use this tool when you need to retrieve and analyze web content
Usage notes: 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. - 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 - The URL must be a fully-formed valid URL
- HTTP URLs will be automatically upgraded to HTTPS - 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 - This tool is read-only and does not modify any files
- Results may be summarized if the content is very large - Results may be summarized if the content is very large
+6
View File
@@ -11,6 +11,12 @@ await fs.mkdir(dir, { recursive: true })
afterAll(() => { afterAll(() => {
fsSync.rmSync(dir, { recursive: true, force: true }) 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_DATA_HOME"] = path.join(dir, "share")
process.env["XDG_CACHE_HOME"] = path.join(dir, "cache") process.env["XDG_CACHE_HOME"] = path.join(dir, "cache")
process.env["XDG_CONFIG_HOME"] = path.join(dir, "config") process.env["XDG_CONFIG_HOME"] = path.join(dir, "config")
+79 -25
View File
@@ -1,9 +1,26 @@
import { test, expect } from "bun:test" import { test, expect } from "bun:test"
import { Skill } from "../../src/skill" import { Skill } from "../../src/skill"
import { SystemPrompt } from "../../src/session/system"
import { Instance } from "../../src/project/instance" import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture" import { tmpdir } from "../fixture/fixture"
import path from "path" 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 () => { test("discovers skills from .opencode/skill/ directory", async () => {
await using tmp = await tmpdir({ await using tmp = await tmpdir({
@@ -30,9 +47,10 @@ Instructions here.
fn: async () => { fn: async () => {
const skills = await Skill.all() const skills = await Skill.all()
expect(skills.length).toBe(1) expect(skills.length).toBe(1)
expect(skills[0].name).toBe("test-skill") const testSkill = skills.find((s) => s.name === "test-skill")
expect(skills[0].description).toBe("A test skill for verification.") expect(testSkill).toBeDefined()
expect(skills[0].location).toContain("skill/test-skill/SKILL.md") 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({ await using tmp = await tmpdir({
git: true, git: true,
init: async (dir) => { 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( await Bun.write(
path.join(skillDir, "SKILL.md"), path.join(skillDir1, "SKILL.md"),
`--- `---
name: my-skill name: skill-one
description: Another test skill. 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, directory: tmp.path,
fn: async () => { fn: async () => {
const skills = await Skill.all() const skills = await Skill.all()
expect(skills.length).toBe(1) expect(skills.length).toBe(2)
expect(skills[0].name).toBe("my-skill") 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 () => { test("discovers skills from .claude/skills/ directory", async () => {
await using tmp = await tmpdir({ await using tmp = await tmpdir({
git: true, git: true,
@@ -124,8 +142,44 @@ description: A skill in the .claude/skills directory.
fn: async () => { fn: async () => {
const skills = await Skill.all() const skills = await Skill.all()
expect(skills.length).toBe(1) expect(skills.length).toBe(1)
expect(skills[0].name).toBe("claude-skill") const claudeSkill = skills.find((s) => s.name === "claude-skill")
expect(skills[0].location).toContain(".claude/skills/claude-skill/SKILL.md") 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 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.0.208", "version": "1.0.217",
"type": "module", "type": "module",
"scripts": { "scripts": {
"typecheck": "tsgo --noEmit", "typecheck": "tsgo --noEmit",
+1
View File
@@ -28,6 +28,7 @@ export type PluginInput = {
project: Project project: Project
directory: string directory: string
worktree: string worktree: string
serverUrl: URL
$: BunShell $: BunShell
} }
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "1.0.208", "version": "1.0.217",
"type": "module", "type": "module",
"scripts": { "scripts": {
"typecheck": "tsgo --noEmit", "typecheck": "tsgo --noEmit",
+11 -1
View File
@@ -1228,6 +1228,7 @@ export class Session extends HeyApiClient {
[key: string]: boolean [key: string]: boolean
} }
system?: string system?: string
variant?: string
parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput> parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
}, },
options?: Options<never, ThrowOnError>, options?: Options<never, ThrowOnError>,
@@ -1245,6 +1246,7 @@ export class Session extends HeyApiClient {
{ in: "body", key: "noReply" }, { in: "body", key: "noReply" },
{ in: "body", key: "tools" }, { in: "body", key: "tools" },
{ in: "body", key: "system" }, { in: "body", key: "system" },
{ in: "body", key: "variant" },
{ in: "body", key: "parts" }, { in: "body", key: "parts" },
], ],
}, },
@@ -1314,6 +1316,7 @@ export class Session extends HeyApiClient {
[key: string]: boolean [key: string]: boolean
} }
system?: string system?: string
variant?: string
parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput> parts?: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
}, },
options?: Options<never, ThrowOnError>, options?: Options<never, ThrowOnError>,
@@ -1331,6 +1334,7 @@ export class Session extends HeyApiClient {
{ in: "body", key: "noReply" }, { in: "body", key: "noReply" },
{ in: "body", key: "tools" }, { in: "body", key: "tools" },
{ in: "body", key: "system" }, { in: "body", key: "system" },
{ in: "body", key: "variant" },
{ in: "body", key: "parts" }, { in: "body", key: "parts" },
], ],
}, },
@@ -1362,6 +1366,7 @@ export class Session extends HeyApiClient {
model?: string model?: string
arguments?: string arguments?: string
command?: string command?: string
variant?: string
}, },
options?: Options<never, ThrowOnError>, options?: Options<never, ThrowOnError>,
) { ) {
@@ -1377,6 +1382,7 @@ export class Session extends HeyApiClient {
{ in: "body", key: "model" }, { in: "body", key: "model" },
{ in: "body", key: "arguments" }, { in: "body", key: "arguments" },
{ in: "body", key: "command" }, { in: "body", key: "command" },
{ in: "body", key: "variant" },
], ],
}, },
], ],
@@ -1823,13 +1829,15 @@ export class Find extends HeyApiClient {
/** /**
* Find files * 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>( public files<ThrowOnError extends boolean = false>(
parameters: { parameters: {
directory?: string directory?: string
query: string query: string
dirs?: "true" | "false" dirs?: "true" | "false"
type?: "file" | "directory"
limit?: number
}, },
options?: Options<never, ThrowOnError>, options?: Options<never, ThrowOnError>,
) { ) {
@@ -1841,6 +1849,8 @@ export class Find extends HeyApiClient {
{ in: "query", key: "directory" }, { in: "query", key: "directory" },
{ in: "query", key: "query" }, { in: "query", key: "query" },
{ in: "query", key: "dirs" }, { in: "query", key: "dirs" },
{ in: "query", key: "type" },
{ in: "query", key: "limit" },
], ],
}, },
], ],
+18
View File
@@ -90,6 +90,7 @@ export type UserMessage = {
tools?: { tools?: {
[key: string]: boolean [key: string]: boolean
} }
variant?: string
} }
export type ProviderAuthError = { export type ProviderAuthError = {
@@ -969,6 +970,10 @@ export type KeybindsConfig = {
* Previous agent * Previous agent
*/ */
agent_cycle_reverse?: string agent_cycle_reverse?: string
/**
* Cycle model variants
*/
variant_cycle?: string
/** /**
* Clear input field * Clear input field
*/ */
@@ -1712,6 +1717,11 @@ export type Command = {
subtask?: boolean subtask?: boolean
} }
export type Variant = {
disabled: boolean
[key: string]: unknown | boolean
}
export type Model = { export type Model = {
id: string id: string
providerID: string providerID: string
@@ -1775,6 +1785,9 @@ export type Model = {
[key: string]: string [key: string]: string
} }
release_date: string release_date: string
variants?: {
[key: string]: Variant
}
} }
export type Provider = { export type Provider = {
@@ -2944,6 +2957,7 @@ export type SessionPromptData = {
[key: string]: boolean [key: string]: boolean
} }
system?: string system?: string
variant?: string
parts: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput> parts: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
} }
path: { path: {
@@ -3127,6 +3141,7 @@ export type SessionPromptAsyncData = {
[key: string]: boolean [key: string]: boolean
} }
system?: string system?: string
variant?: string
parts: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput> parts: Array<TextPartInput | FilePartInput | AgentPartInput | SubtaskPartInput>
} }
path: { path: {
@@ -3170,6 +3185,7 @@ export type SessionCommandData = {
model?: string model?: string
arguments: string arguments: string
command: string command: string
variant?: string
} }
path: { path: {
/** /**
@@ -3633,6 +3649,8 @@ export type FindFilesData = {
directory?: string directory?: string
query: string query: string
dirs?: "true" | "false" dirs?: "true" | "false"
type?: "file" | "directory"
limit?: number
} }
url: "/find/file" url: "/find/file"
} }
+54 -1
View File
@@ -2048,6 +2048,9 @@
"system": { "system": {
"type": "string" "type": "string"
}, },
"variant": {
"type": "string"
},
"parts": { "parts": {
"type": "array", "type": "array",
"items": { "items": {
@@ -2420,6 +2423,9 @@
"system": { "system": {
"type": "string" "type": "string"
}, },
"variant": {
"type": "string"
},
"parts": { "parts": {
"type": "array", "type": "array",
"items": { "items": {
@@ -2541,6 +2547,9 @@
}, },
"command": { "command": {
"type": "string" "type": "string"
},
"variant": {
"type": "string"
} }
}, },
"required": ["arguments", "command"] "required": ["arguments", "command"]
@@ -3556,10 +3565,27 @@
"type": "string", "type": "string",
"enum": ["true", "false"] "enum": ["true", "false"]
} }
},
{
"in": "query",
"name": "type",
"schema": {
"type": "string",
"enum": ["file", "directory"]
}
},
{
"in": "query",
"name": "limit",
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 200
}
} }
], ],
"summary": "Find files", "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.",
"responses": { "responses": {
"200": { "200": {
"description": "File paths", "description": "File paths",
@@ -5265,6 +5291,9 @@
"additionalProperties": { "additionalProperties": {
"type": "boolean" "type": "boolean"
} }
},
"variant": {
"type": "string"
} }
}, },
"required": ["id", "sessionID", "role", "time", "agent", "model"] "required": ["id", "sessionID", "role", "time", "agent", "model"]
@@ -7496,6 +7525,11 @@
"default": "shift+tab", "default": "shift+tab",
"type": "string" "type": "string"
}, },
"variant_cycle": {
"description": "Cycle model variants",
"default": "ctrl+t",
"type": "string"
},
"input_clear": { "input_clear": {
"description": "Clear input field", "description": "Clear input field",
"default": "ctrl+c", "default": "ctrl+c",
@@ -8914,6 +8948,16 @@
}, },
"required": ["name", "template"] "required": ["name", "template"]
}, },
"Variant": {
"type": "object",
"properties": {
"disabled": {
"type": "boolean"
}
},
"required": ["disabled"],
"additionalProperties": {}
},
"Model": { "Model": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -9103,6 +9147,15 @@
}, },
"release_date": { "release_date": {
"type": "string" "type": "string"
},
"variants": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"$ref": "#/components/schemas/Variant"
}
} }
}, },
"required": [ "required": [
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.0.208", "version": "1.0.217",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "bun run src/index.ts", "dev": "bun run src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "1.0.208", "version": "1.0.217",
"type": "module", "type": "module",
"exports": { "exports": {
"./*": "./src/components/*.tsx", "./*": "./src/components/*.tsx",

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