Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c48e6a116 | ||
|
|
ef266b2c74 | ||
|
|
0a1cdc7a58 | ||
|
|
2dec956a17 | ||
|
|
ef8388f0ee | ||
|
|
e5c5b5e872 | ||
|
|
a1c9a1b8c5 | ||
|
|
76b012139a | ||
|
|
02e5a19242 | ||
|
|
af967648cb | ||
|
|
504a668a26 | ||
|
|
5efb1c7b2d | ||
|
|
fd973d242e | ||
|
|
c3d8672753 | ||
|
|
fe8ef041f6 | ||
|
|
c841de947e | ||
|
|
825dfd48b1 | ||
|
|
923d114ffa | ||
|
|
b157fd10a7 | ||
|
|
67ebe68160 | ||
|
|
7b63c14154 | ||
|
|
cdc11cde2e | ||
|
|
9721223b7e | ||
|
|
35a626e711 | ||
|
|
bb7b0ff221 | ||
|
|
68b4038196 | ||
|
|
3109214900 | ||
|
|
86ccc3409b | ||
|
|
a89089c88f | ||
|
|
e617c5d689 | ||
|
|
31983ca5ff | ||
|
|
59e3b7409f | ||
|
|
b7ce46f7a1 | ||
|
|
82b8d8fa5d | ||
|
|
77c837eb1a | ||
|
|
db77cc9845 | ||
|
|
68043edae6 | ||
|
|
337681dbbf | ||
|
|
66afc034d1 | ||
|
|
11ab8de59f | ||
|
|
5f074edc3a | ||
|
|
56b5cdf883 | ||
|
|
fb0e1e4d8d | ||
|
|
b745b1593f | ||
|
|
7376c3f8e7 | ||
|
|
831e9bce51 | ||
|
|
5de73abd82 | ||
|
|
3adbbc1b23 | ||
|
|
c6c29b3dcf | ||
|
|
a687d7c15f | ||
|
|
0c6da69f39 | ||
|
|
c4930eb6b2 | ||
|
|
a24549fce7 | ||
|
|
c0f9b13630 | ||
|
|
98fd53fd5f | ||
|
|
5b02a3029e | ||
|
|
94e851c2a2 | ||
|
|
1658a3ff59 | ||
|
|
9c8bc64138 | ||
|
|
80f704ebbf | ||
|
|
4dae6d1fcf | ||
|
|
5d2cab39da | ||
|
|
6963f96d4b | ||
|
|
05a9e7ce7a | ||
|
|
896d18ab3f | ||
|
|
893888536a | ||
|
|
c6221fc8b3 | ||
|
|
ae67f43ff0 | ||
|
|
76880dce0d | ||
|
|
aafffb5b4b | ||
|
|
a71c9e3f2e | ||
|
|
0156f03e0e | ||
|
|
e0bb96a9f9 | ||
|
|
82e5d6d458 | ||
|
|
a4411c21b6 | ||
|
|
9d61370ac4 | ||
|
|
f3febd6e39 | ||
|
|
f12d55bf1e | ||
|
|
0c19b71f42 | ||
|
|
70fa66397e | ||
|
|
6e8cd3174c | ||
|
|
5bfffbe083 | ||
|
|
29d8557d41 | ||
|
|
ffd20b4477 | ||
|
|
2abaa46e23 | ||
|
|
0cbbb20d22 | ||
|
|
81c5e7b9ed | ||
|
|
ddf4897eaa | ||
|
|
040939fb72 | ||
|
|
f89b83a6d7 | ||
|
|
82a876da4d | ||
|
|
69a15ae9c1 | ||
|
|
18c8e5f451 | ||
|
|
ba3a1cfa0b | ||
|
|
d8563160f7 | ||
|
|
4a9ff9412e | ||
|
|
d6db6ff198 | ||
|
|
79c263494f | ||
|
|
1b5bf32ce5 | ||
|
|
2e972b3fdc | ||
|
|
d70e9fb01e | ||
|
|
fc082a0f14 | ||
|
|
953e4e9446 | ||
|
|
7ea0d37ee3 | ||
|
|
e35d97f9d7 | ||
|
|
2c0d9a46cb | ||
|
|
2fe7a7f2d3 | ||
|
|
8a2f4ddf70 | ||
|
|
7a94d7a2c5 | ||
|
|
de28fafb47 | ||
|
|
9d485dd307 | ||
|
|
613813ac12 | ||
|
|
7617f59441 | ||
|
|
7aecb43e84 | ||
|
|
21eba5f987 | ||
|
|
c523ca4127 | ||
|
|
685f3ea324 | ||
|
|
4667d57e3c | ||
|
|
e6b9988fa4 | ||
|
|
3c02d5d338 | ||
|
|
bfb9787361 | ||
|
|
1bcc72c477 | ||
|
|
4385fa4dd7 | ||
|
|
2b054bec95 | ||
|
|
2cdc88d295 | ||
|
|
f8fb08b3b4 | ||
|
|
ed06de5e30 | ||
|
|
52b99622ad | ||
|
|
a15397cd89 | ||
|
|
da394439a1 | ||
|
|
390b0a79b3 | ||
|
|
b2f45d574f | ||
|
|
1e2ef07c97 | ||
|
|
664e6bf2d0 | ||
|
|
160c8ab7cc | ||
|
|
1626341a4a | ||
|
|
61ddd1716d | ||
|
|
053a10e515 | ||
|
|
e1c1b1340b | ||
|
|
7a5fbdf67c | ||
|
|
9afc451020 | ||
|
|
f4fdf0eb03 | ||
|
|
505068d5a6 | ||
|
|
2e10ffac6b | ||
|
|
4abaa052db | ||
|
|
1bcf8d8806 | ||
|
|
25c68c8061 | ||
|
|
b0e4408ecf | ||
|
|
8416db03ef | ||
|
|
d5b47d9128 | ||
|
|
634559760a | ||
|
|
155ba794cf | ||
|
|
f1ab427f0e | ||
|
|
2333af6ed3 | ||
|
|
54588b4570 | ||
|
|
26e7043718 | ||
|
|
dd569c927a | ||
|
|
cf38884778 | ||
|
|
2946a6d9a7 | ||
|
|
3522c460e3 | ||
|
|
b6a264819e | ||
|
|
46c7a41d5f | ||
|
|
7cc4b24ac2 | ||
|
|
281ce4c0c3 | ||
|
|
f59d274d0f | ||
|
|
8886c78dce | ||
|
|
d9f0f58277 | ||
|
|
effa7b45cf | ||
|
|
b307075063 | ||
|
|
aaf9a5d434 | ||
|
|
e9c2f1f3f3 | ||
|
|
7469cba7cf | ||
|
|
5420702f69 | ||
|
|
583751ecae | ||
|
|
d0a1b5ef96 | ||
|
|
42f2bc7199 | ||
|
|
603dae562a | ||
|
|
650bd76370 | ||
|
|
8aa3520683 | ||
|
|
5b5b8c57d9 | ||
|
|
f057b22e20 | ||
|
|
388d40e41f | ||
|
|
f397c92ddf | ||
|
|
6f9bea4e1f | ||
|
|
5c49b4cbfc | ||
|
|
b746e831e2 | ||
|
|
2178deef91 | ||
|
|
b1d2fb5319 |
@@ -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 }}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ jobs:
|
|||||||
Please check all the code changes in this pull request against the style guide, also look for any bugs if they exist. Diffs are important but make sure you read the entire file to get proper context. Make it clear the suggestions are merely suggestions and the human can decide what to do
|
Please check all the code changes in this pull request against the style guide, also look for any bugs if they exist. Diffs are important but make sure you read the entire file to get proper context. Make it clear the suggestions are merely suggestions and the human can decide what to do
|
||||||
|
|
||||||
When critiquing code against the style guide, be sure that the code is ACTUALLY in violation, don't complain about else statements if they already use early returns there. You may complain about excessive nesting though, regardless of else statement usage.
|
When critiquing code against the style guide, be sure that the code is ACTUALLY in violation, don't complain about else statements if they already use early returns there. You may complain about excessive nesting though, regardless of else statement usage.
|
||||||
When critiquing code style don't be a zealot, we don't like "let" statements but sometimes they are the simpliest option, if someone does a bunch of nesting with let, they should consider using iife (see packages/opencode/src/util.iife.ts)
|
When critiquing code style don't be a zealot, we don't like "let" statements but sometimes they are the simplest option, if someone does a bunch of nesting with let, they should consider using iife (see packages/opencode/src/util.iife.ts)
|
||||||
|
|
||||||
Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block.
|
Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block.
|
||||||
If you are writing suggested fixes, BE SURE THAT the change you are recommending is actually valid typescript, often I have seen missing closing "}" or other syntax errors.
|
If you are writing suggested fixes, BE SURE THAT the change you are recommending is actually valid typescript, often I have seen missing closing "}" or other syntax errors.
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
name: "Auto-close stale issues"
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "30 1 * * *" # Daily at 1:30 AM
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
DAYS_BEFORE_STALE: 90
|
||||||
|
DAYS_BEFORE_CLOSE: 7
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v10
|
||||||
|
with:
|
||||||
|
days-before-stale: ${{ env.DAYS_BEFORE_STALE }}
|
||||||
|
days-before-close: ${{ env.DAYS_BEFORE_CLOSE }}
|
||||||
|
stale-issue-label: "stale"
|
||||||
|
close-issue-message: |
|
||||||
|
[automated] Closing due to ${{ env.DAYS_BEFORE_STALE }}+ days of inactivity.
|
||||||
|
|
||||||
|
Feel free to reopen if you still need this!
|
||||||
|
stale-issue-message: |
|
||||||
|
[automated] This issue has had no activity for ${{ env.DAYS_BEFORE_STALE }} days.
|
||||||
|
|
||||||
|
It will be closed in ${{ env.DAYS_BEFORE_CLOSE }} days if there's no new activity.
|
||||||
|
remove-stale-when-updated: true
|
||||||
|
exempt-issue-labels: "pinned,security,feature-request,on-hold"
|
||||||
|
start-date: "2025-12-27"
|
||||||
@@ -2,8 +2,8 @@ name: "sync-zed-extension"
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
# release:
|
release:
|
||||||
# types: [published]
|
types: [published]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
zed:
|
zed:
|
||||||
@@ -31,4 +31,4 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
./script/sync-zed.ts ${{ steps.get_tag.outputs.tag }}
|
./script/sync-zed.ts ${{ steps.get_tag.outputs.tag }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
ZED_EXTENSIONS_PAT: ${{ secrets.ZED_EXTENSIONS_PAT }}
|
||||||
|
|||||||
@@ -20,3 +20,7 @@ opencode.json
|
|||||||
a.out
|
a.out
|
||||||
target
|
target
|
||||||
.scripts
|
.scripts
|
||||||
|
|
||||||
|
# Local dev files
|
||||||
|
opencode-dev
|
||||||
|
logs/
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
description: ALWAYS use this when writing docs
|
description: ALWAYS use this when writing docs
|
||||||
|
color: "#38A3EE"
|
||||||
---
|
---
|
||||||
|
|
||||||
You are an expert technical documentation writer
|
You are an expert technical documentation writer
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
mode: primary
|
mode: primary
|
||||||
hidden: true
|
hidden: true
|
||||||
model: opencode/claude-haiku-4-5
|
model: opencode/claude-haiku-4-5
|
||||||
|
color: "#44BA81"
|
||||||
tools:
|
tools:
|
||||||
"*": false
|
"*": false
|
||||||
"github-triage": true
|
"github-triage": true
|
||||||
|
|||||||
@@ -34,6 +34,36 @@ Want to take on an issue? Leave a comment and a maintainer may assign it to you
|
|||||||
bun dev
|
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)
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ you can switch between these using the `Tab` key.
|
|||||||
- Asks permission before running bash commands
|
- Asks permission before running bash commands
|
||||||
- Ideal for exploring unfamiliar codebases or planning changes
|
- Ideal for exploring unfamiliar codebases or planning changes
|
||||||
|
|
||||||
Also, included is a **general** subagent for complex searches and multi-step tasks.
|
Also, included is a **general** subagent for complex searches and multistep tasks.
|
||||||
This is used internally and can be invoked using `@general` in messages.
|
This is used internally and can be invoked using `@general` in messages.
|
||||||
|
|
||||||
Learn more about [agents](https://opencode.ai/docs/agents).
|
Learn more about [agents](https://opencode.ai/docs/agents).
|
||||||
@@ -98,7 +98,7 @@ If you are working on a project that's related to OpenCode and is using "opencod
|
|||||||
|
|
||||||
### FAQ
|
### FAQ
|
||||||
|
|
||||||
#### How is this different than Claude Code?
|
#### How is this different from Claude Code?
|
||||||
|
|
||||||
It's very similar to Claude Code in terms of capability. Here are the key differences:
|
It's very similar to Claude Code in terms of capability. Here are the key differences:
|
||||||
|
|
||||||
|
|||||||
@@ -180,3 +180,8 @@
|
|||||||
| 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) |
|
| 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) |
|
||||||
| 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) |
|
| 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) |
|
||||||
| 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) |
|
| 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) |
|
||||||
|
| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) |
|
||||||
|
| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) |
|
||||||
|
| 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) |
|
||||||
|
| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) |
|
||||||
|
| 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) |
|
||||||
|
|||||||
@@ -5,13 +5,6 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/cerebras": "1.0.33",
|
|
||||||
"@ai-sdk/cohere": "2.0.21",
|
|
||||||
"@ai-sdk/deepinfra": "1.0.30",
|
|
||||||
"@ai-sdk/gateway": "2.0.23",
|
|
||||||
"@ai-sdk/groq": "2.0.33",
|
|
||||||
"@ai-sdk/perplexity": "2.0.22",
|
|
||||||
"@ai-sdk/togetherai": "1.0.30",
|
|
||||||
"@aws-sdk/client-s3": "3.933.0",
|
"@aws-sdk/client-s3": "3.933.0",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
@@ -29,7 +22,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.0.199",
|
"version": "1.0.209",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -77,7 +70,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.0.199",
|
"version": "1.0.209",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -105,7 +98,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.0.199",
|
"version": "1.0.209",
|
||||||
"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",
|
||||||
@@ -132,7 +125,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.0.199",
|
"version": "1.0.209",
|
||||||
"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",
|
||||||
@@ -156,7 +149,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.0.199",
|
"version": "1.0.209",
|
||||||
"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",
|
||||||
@@ -180,7 +173,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.0.199",
|
"version": "1.0.209",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@solid-primitives/storage": "catalog:",
|
"@solid-primitives/storage": "catalog:",
|
||||||
@@ -207,7 +200,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.0.199",
|
"version": "1.0.209",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
@@ -236,7 +229,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.0.199",
|
"version": "1.0.209",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -252,7 +245,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.0.199",
|
"version": "1.0.209",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -261,16 +254,23 @@
|
|||||||
"@actions/github": "6.0.1",
|
"@actions/github": "6.0.1",
|
||||||
"@agentclientprotocol/sdk": "0.5.1",
|
"@agentclientprotocol/sdk": "0.5.1",
|
||||||
"@ai-sdk/amazon-bedrock": "3.0.57",
|
"@ai-sdk/amazon-bedrock": "3.0.57",
|
||||||
"@ai-sdk/anthropic": "2.0.50",
|
"@ai-sdk/anthropic": "2.0.56",
|
||||||
"@ai-sdk/azure": "2.0.73",
|
"@ai-sdk/azure": "2.0.73",
|
||||||
|
"@ai-sdk/cerebras": "1.0.33",
|
||||||
|
"@ai-sdk/cohere": "2.0.21",
|
||||||
|
"@ai-sdk/deepinfra": "1.0.30",
|
||||||
|
"@ai-sdk/gateway": "2.0.23",
|
||||||
"@ai-sdk/google": "2.0.44",
|
"@ai-sdk/google": "2.0.44",
|
||||||
"@ai-sdk/google-vertex": "3.0.81",
|
"@ai-sdk/google-vertex": "3.0.81",
|
||||||
|
"@ai-sdk/groq": "2.0.33",
|
||||||
"@ai-sdk/mcp": "0.0.8",
|
"@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.27",
|
||||||
|
"@ai-sdk/perplexity": "2.0.22",
|
||||||
"@ai-sdk/provider": "2.0.0",
|
"@ai-sdk/provider": "2.0.0",
|
||||||
"@ai-sdk/provider-utils": "3.0.18",
|
"@ai-sdk/provider-utils": "3.0.19",
|
||||||
|
"@ai-sdk/togetherai": "1.0.30",
|
||||||
"@ai-sdk/xai": "2.0.42",
|
"@ai-sdk/xai": "2.0.42",
|
||||||
"@clack/prompts": "1.0.0-alpha.1",
|
"@clack/prompts": "1.0.0-alpha.1",
|
||||||
"@hono/standard-validator": "0.1.5",
|
"@hono/standard-validator": "0.1.5",
|
||||||
@@ -292,6 +292,7 @@
|
|||||||
"@standard-schema/spec": "1.0.0",
|
"@standard-schema/spec": "1.0.0",
|
||||||
"@zip.js/zip.js": "2.7.62",
|
"@zip.js/zip.js": "2.7.62",
|
||||||
"ai": "catalog:",
|
"ai": "catalog:",
|
||||||
|
"bonjour-service": "1.3.0",
|
||||||
"bun-pty": "0.4.2",
|
"bun-pty": "0.4.2",
|
||||||
"chokidar": "4.0.3",
|
"chokidar": "4.0.3",
|
||||||
"clipboardy": "4.0.0",
|
"clipboardy": "4.0.0",
|
||||||
@@ -346,7 +347,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.0.199",
|
"version": "1.0.209",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
@@ -366,7 +367,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.0.199",
|
"version": "1.0.209",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hey-api/openapi-ts": "0.88.1",
|
"@hey-api/openapi-ts": "0.88.1",
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
@@ -377,7 +378,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.0.199",
|
"version": "1.0.209",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -390,7 +391,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.0.199",
|
"version": "1.0.209",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -402,8 +403,10 @@
|
|||||||
"@solidjs/meta": "catalog:",
|
"@solidjs/meta": "catalog:",
|
||||||
"@typescript/native-preview": "catalog:",
|
"@typescript/native-preview": "catalog:",
|
||||||
"fuzzysort": "catalog:",
|
"fuzzysort": "catalog:",
|
||||||
|
"katex": "0.16.27",
|
||||||
"luxon": "catalog:",
|
"luxon": "catalog:",
|
||||||
"marked": "catalog:",
|
"marked": "catalog:",
|
||||||
|
"marked-katex-extension": "5.1.6",
|
||||||
"marked-shiki": "catalog:",
|
"marked-shiki": "catalog:",
|
||||||
"remeda": "catalog:",
|
"remeda": "catalog:",
|
||||||
"shiki": "catalog:",
|
"shiki": "catalog:",
|
||||||
@@ -415,6 +418,7 @@
|
|||||||
"@tailwindcss/vite": "catalog:",
|
"@tailwindcss/vite": "catalog:",
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
|
"@types/katex": "0.16.7",
|
||||||
"@types/luxon": "catalog:",
|
"@types/luxon": "catalog:",
|
||||||
"tailwindcss": "catalog:",
|
"tailwindcss": "catalog:",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
@@ -425,7 +429,7 @@
|
|||||||
},
|
},
|
||||||
"packages/util": {
|
"packages/util": {
|
||||||
"name": "@opencode-ai/util",
|
"name": "@opencode-ai/util",
|
||||||
"version": "1.0.199",
|
"version": "1.0.209",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -436,7 +440,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.0.199",
|
"version": "1.0.209",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -565,7 +569,7 @@
|
|||||||
|
|
||||||
"@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
|
"@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
|
||||||
|
|
||||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="],
|
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
||||||
|
|
||||||
"@ai-sdk/togetherai": ["@ai-sdk/togetherai@1.0.30", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.29", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9bxQbIXnWSN4bNismrza3NvIo+ui/Y3pj3UN6e9vCszCWFCN45RgISi4oDe10RqmzaJ/X8cfO/Tem+K8MT3wGQ=="],
|
"@ai-sdk/togetherai": ["@ai-sdk/togetherai@1.0.30", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.29", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9bxQbIXnWSN4bNismrza3NvIo+ui/Y3pj3UN6e9vCszCWFCN45RgISi4oDe10RqmzaJ/X8cfO/Tem+K8MT3wGQ=="],
|
||||||
|
|
||||||
@@ -1081,6 +1085,8 @@
|
|||||||
|
|
||||||
"@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="],
|
"@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="],
|
||||||
|
|
||||||
|
"@leichtgewicht/ip-codec": ["@leichtgewicht/ip-codec@2.0.5", "", {}, "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="],
|
||||||
|
|
||||||
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
|
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
|
||||||
|
|
||||||
"@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
|
"@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
|
||||||
@@ -1771,6 +1777,8 @@
|
|||||||
|
|
||||||
"@types/jsonwebtoken": ["@types/jsonwebtoken@8.5.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg=="],
|
"@types/jsonwebtoken": ["@types/jsonwebtoken@8.5.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg=="],
|
||||||
|
|
||||||
|
"@types/katex": ["@types/katex@0.16.7", "", {}, "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="],
|
||||||
|
|
||||||
"@types/luxon": ["@types/luxon@3.7.1", "", {}, "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg=="],
|
"@types/luxon": ["@types/luxon@3.7.1", "", {}, "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg=="],
|
||||||
|
|
||||||
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||||
@@ -2003,6 +2011,8 @@
|
|||||||
|
|
||||||
"body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="],
|
"body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="],
|
||||||
|
|
||||||
|
"bonjour-service": ["bonjour-service@1.3.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA=="],
|
||||||
|
|
||||||
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
||||||
|
|
||||||
"bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="],
|
"bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="],
|
||||||
@@ -2247,6 +2257,8 @@
|
|||||||
|
|
||||||
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
|
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
|
||||||
|
|
||||||
|
"dns-packet": ["dns-packet@5.6.1", "", { "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" } }, "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw=="],
|
||||||
|
|
||||||
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
|
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
|
||||||
|
|
||||||
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
|
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
|
||||||
@@ -2779,6 +2791,8 @@
|
|||||||
|
|
||||||
"jwt-decode": ["jwt-decode@3.1.2", "", {}, "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="],
|
"jwt-decode": ["jwt-decode@3.1.2", "", {}, "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="],
|
||||||
|
|
||||||
|
"katex": ["katex@0.16.27", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw=="],
|
||||||
|
|
||||||
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
|
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
|
||||||
|
|
||||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||||
@@ -2869,6 +2883,8 @@
|
|||||||
|
|
||||||
"marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
|
"marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
|
||||||
|
|
||||||
|
"marked-katex-extension": ["marked-katex-extension@5.1.6", "", { "peerDependencies": { "katex": ">=0.16 <0.17", "marked": ">=4 <18" } }, "sha512-vYpLXwmlIDKILIhJtiRTgdyZRn5sEYdFBuTmbpjD7lbCIzg0/DWyK3HXIntN3Tp8zV6hvOUgpZNLWRCgWVc24A=="],
|
||||||
|
|
||||||
"marked-shiki": ["marked-shiki@1.2.1", "", { "peerDependencies": { "marked": ">=7.0.0", "shiki": ">=1.0.0" } }, "sha512-yHxYQhPY5oYaIRnROn98foKhuClark7M373/VpLxiy5TrDu9Jd/LsMwo8w+U91Up4oDb9IXFrP0N1MFRz8W/DQ=="],
|
"marked-shiki": ["marked-shiki@1.2.1", "", { "peerDependencies": { "marked": ">=7.0.0", "shiki": ">=1.0.0" } }, "sha512-yHxYQhPY5oYaIRnROn98foKhuClark7M373/VpLxiy5TrDu9Jd/LsMwo8w+U91Up4oDb9IXFrP0N1MFRz8W/DQ=="],
|
||||||
|
|
||||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||||
@@ -3023,6 +3039,8 @@
|
|||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"multicast-dns": ["multicast-dns@7.2.5", "", { "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" }, "bin": { "multicast-dns": "cli.js" } }, "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg=="],
|
||||||
|
|
||||||
"mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="],
|
"mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="],
|
||||||
|
|
||||||
"mysql2": ["mysql2@3.14.4", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-Cs/jx3WZPNrYHVz+Iunp9ziahaG5uFMvD2R8Zlmc194AqXNxt9HBNu7ZsPYrUtmJsF0egETCWIdMIYAwOGjL1w=="],
|
"mysql2": ["mysql2@3.14.4", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-Cs/jx3WZPNrYHVz+Iunp9ziahaG5uFMvD2R8Zlmc194AqXNxt9HBNu7ZsPYrUtmJsF0egETCWIdMIYAwOGjL1w=="],
|
||||||
@@ -3595,6 +3613,8 @@
|
|||||||
|
|
||||||
"three": ["three@0.177.0", "", {}, "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg=="],
|
"three": ["three@0.177.0", "", {}, "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg=="],
|
||||||
|
|
||||||
|
"thunky": ["thunky@1.1.0", "", {}, "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="],
|
||||||
|
|
||||||
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
|
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
|
||||||
|
|
||||||
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||||
@@ -3887,38 +3907,24 @@
|
|||||||
|
|
||||||
"@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/cerebras/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
|
||||||
|
|
||||||
"@ai-sdk/cohere/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
|
||||||
|
|
||||||
"@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
|
"@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
|
||||||
|
|
||||||
"@ai-sdk/deepinfra/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
"@ai-sdk/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/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
|
||||||
|
|
||||||
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.50", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-21PaHfoLmouOXXNINTsZJsMw+wE5oLR2He/1kq/sKokTVKyq7ObGT1LDk6ahwxaz/GoaNaGankMh+EgVcdv2Cw=="],
|
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.50", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-21PaHfoLmouOXXNINTsZJsMw+wE5oLR2He/1kq/sKokTVKyq7ObGT1LDk6ahwxaz/GoaNaGankMh+EgVcdv2Cw=="],
|
||||||
|
|
||||||
"@ai-sdk/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
"@ai-sdk/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/mcp/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
|
||||||
|
|
||||||
"@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
|
||||||
|
|
||||||
"@ai-sdk/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=="],
|
||||||
|
|
||||||
"@ai-sdk/perplexity/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
|
||||||
|
|
||||||
"@ai-sdk/togetherai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
|
"@ai-sdk/togetherai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
|
||||||
|
|
||||||
"@ai-sdk/togetherai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
|
||||||
|
|
||||||
"@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
|
"@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
|
||||||
|
|
||||||
"@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
|
|
||||||
|
|
||||||
"@astrojs/cloudflare/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
|
"@astrojs/cloudflare/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
|
||||||
|
|
||||||
"@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="],
|
"@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="],
|
||||||
@@ -4273,6 +4279,8 @@
|
|||||||
|
|
||||||
"jsonwebtoken/jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="],
|
"jsonwebtoken/jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="],
|
||||||
|
|
||||||
|
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||||
|
|
||||||
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
|
||||||
|
|
||||||
"lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
"lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
@@ -4297,7 +4305,7 @@
|
|||||||
|
|
||||||
"nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
"nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||||
|
|
||||||
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.50", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-21PaHfoLmouOXXNINTsZJsMw+wE5oLR2He/1kq/sKokTVKyq7ObGT1LDk6ahwxaz/GoaNaGankMh+EgVcdv2Cw=="],
|
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XHJKu0Yvfu9SPzRfsAFESa+9T7f2YJY6TxykKMfRsAwpeWAiX/Gbx5J5uM15AzYC3Rw8tVP3oH+j7jEivENirQ=="],
|
||||||
|
|
||||||
"opencode/@ai-sdk/openai": ["@ai-sdk/openai@2.0.71", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tg+gj+R0z/On9P4V7hy7/7o04cQPjKGayMCL3gzWD/aNGjAKkhEnaocuNDidSnghizt8g2zJn16cAuAolnW+qQ=="],
|
"opencode/@ai-sdk/openai": ["@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=="],
|
||||||
|
|
||||||
|
|||||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1766532406,
|
"lastModified": 1766996594,
|
||||||
"narHash": "sha256-acLU/ag9VEoKkzOD202QASX25nG1eArXg5A0mHjKgxM=",
|
"narHash": "sha256-SosfgQSqVmOkqVgNYJnxW5FvoIQX4grOcpIKNrIwz4o=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "8142186f001295e5a3239f485c8a49bf2de2695a",
|
"rev": "0744ef1b047f07d31d9962d757ffe38ec14a4d41",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -155,8 +155,18 @@ if [ -z "$requested_version" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
|
# Strip leading 'v' if present
|
||||||
|
requested_version="${requested_version#v}"
|
||||||
url="https://github.com/sst/opencode/releases/download/v${requested_version}/$filename"
|
url="https://github.com/sst/opencode/releases/download/v${requested_version}/$filename"
|
||||||
specific_version=$requested_version
|
specific_version=$requested_version
|
||||||
|
|
||||||
|
# Verify the release exists before downloading
|
||||||
|
http_status=$(curl -sI -o /dev/null -w "%{http_code}" "https://github.com/sst/opencode/releases/tag/v${requested_version}")
|
||||||
|
if [ "$http_status" = "404" ]; then
|
||||||
|
echo -e "${RED}Error: Release v${requested_version} not found${NC}"
|
||||||
|
echo -e "${MUTED}Available releases: https://github.com/sst/opencode/releases${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
print_message() {
|
print_message() {
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": "sha256-hotsyeWJA6/dP6DvZTN1Ak2RSKcsyvXlXPI/jexBHME="
|
"nodeModules": "sha256-2i/QMBzp9MalOXur36mXaDDU8R9G/0dODCODEQOnaCU="
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -67,13 +67,6 @@
|
|||||||
"turbo": "2.5.6"
|
"turbo": "2.5.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/cerebras": "1.0.33",
|
|
||||||
"@ai-sdk/cohere": "2.0.21",
|
|
||||||
"@ai-sdk/deepinfra": "1.0.30",
|
|
||||||
"@ai-sdk/gateway": "2.0.23",
|
|
||||||
"@ai-sdk/groq": "2.0.33",
|
|
||||||
"@ai-sdk/perplexity": "2.0.22",
|
|
||||||
"@ai-sdk/togetherai": "1.0.30",
|
|
||||||
"@aws-sdk/client-s3": "3.933.0",
|
"@aws-sdk/client-s3": "3.933.0",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
|
|||||||
+31
-6
@@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en" style="background-color: var(--background-base)">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
@@ -13,14 +13,39 @@
|
|||||||
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
|
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
|
||||||
<meta property="og:image" content="/social-share.png" />
|
<meta property="og:image" content="/social-share.png" />
|
||||||
<meta property="twitter:image" content="/social-share.png" />
|
<meta property="twitter:image" content="/social-share.png" />
|
||||||
</head>
|
<!-- Theme preload script - applies cached theme to avoid FOUC -->
|
||||||
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
<script id="oc-theme-preload-script">
|
||||||
<script>
|
|
||||||
;(function () {
|
;(function () {
|
||||||
const savedTheme = localStorage.getItem("theme") || "oc-1"
|
var themeId = localStorage.getItem("opencode-theme-id")
|
||||||
document.documentElement.setAttribute("data-theme", savedTheme)
|
if (!themeId) return
|
||||||
|
|
||||||
|
var scheme = localStorage.getItem("opencode-color-scheme") || "system"
|
||||||
|
var isDark = scheme === "dark" || (scheme === "system" && matchMedia("(prefers-color-scheme: dark)").matches)
|
||||||
|
var mode = isDark ? "dark" : "light"
|
||||||
|
|
||||||
|
document.documentElement.dataset.theme = themeId
|
||||||
|
document.documentElement.dataset.colorScheme = mode
|
||||||
|
|
||||||
|
if (themeId === "oc-1") return
|
||||||
|
|
||||||
|
var css = localStorage.getItem("opencode-theme-css-" + themeId + "-" + mode)
|
||||||
|
if (css) {
|
||||||
|
var style = document.createElement("style")
|
||||||
|
style.id = "oc-theme-preload"
|
||||||
|
style.textContent =
|
||||||
|
":root{color-scheme:" +
|
||||||
|
mode +
|
||||||
|
";--text-mix-blend-mode:" +
|
||||||
|
(isDark ? "plus-lighter" : "multiply") +
|
||||||
|
";" +
|
||||||
|
css +
|
||||||
|
"}"
|
||||||
|
document.head.appendChild(style)
|
||||||
|
}
|
||||||
})()
|
})()
|
||||||
</script>
|
</script>
|
||||||
|
</head>
|
||||||
|
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root" class="flex flex-col h-screen"></div>
|
<div id="root" class="flex flex-col h-screen"></div>
|
||||||
<script src="/src/entry.tsx" type="module"></script>
|
<script src="/src/entry.tsx" type="module"></script>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.0.199",
|
"version": "1.0.209",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
+46
-43
@@ -8,6 +8,7 @@ import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
|
|||||||
import { CodeComponentProvider } from "@opencode-ai/ui/context/code"
|
import { CodeComponentProvider } from "@opencode-ai/ui/context/code"
|
||||||
import { Diff } from "@opencode-ai/ui/diff"
|
import { Diff } from "@opencode-ai/ui/diff"
|
||||||
import { Code } from "@opencode-ai/ui/code"
|
import { Code } from "@opencode-ai/ui/code"
|
||||||
|
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"
|
||||||
@@ -38,55 +39,57 @@ const url = iife(() => {
|
|||||||
if (import.meta.env.DEV)
|
if (import.meta.env.DEV)
|
||||||
return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
|
return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
|
||||||
|
|
||||||
return "http://localhost:4096"
|
return window.location.origin
|
||||||
})
|
})
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
<MetaProvider>
|
<MetaProvider>
|
||||||
<Font />
|
<Font />
|
||||||
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
|
<ThemeProvider>
|
||||||
<DialogProvider>
|
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
|
||||||
<MarkedProvider>
|
<DialogProvider>
|
||||||
<DiffComponentProvider component={Diff}>
|
<MarkedProvider>
|
||||||
<CodeComponentProvider component={Code}>
|
<DiffComponentProvider component={Diff}>
|
||||||
<GlobalSDKProvider url={url}>
|
<CodeComponentProvider component={Code}>
|
||||||
<GlobalSyncProvider>
|
<GlobalSDKProvider url={url}>
|
||||||
<LayoutProvider>
|
<GlobalSyncProvider>
|
||||||
<NotificationProvider>
|
<LayoutProvider>
|
||||||
<Router
|
<NotificationProvider>
|
||||||
root={(props) => (
|
<Router
|
||||||
<CommandProvider>
|
root={(props) => (
|
||||||
<Layout>{props.children}</Layout>
|
<CommandProvider>
|
||||||
</CommandProvider>
|
<Layout>{props.children}</Layout>
|
||||||
)}
|
</CommandProvider>
|
||||||
>
|
)}
|
||||||
<Route path="/" component={Home} />
|
>
|
||||||
<Route path="/:dir" component={DirectoryLayout}>
|
<Route path="/" component={Home} />
|
||||||
<Route path="/" component={() => <Navigate href="session" />} />
|
<Route path="/:dir" component={DirectoryLayout}>
|
||||||
<Route
|
<Route path="/" component={() => <Navigate href="session" />} />
|
||||||
path="/session/:id?"
|
<Route
|
||||||
component={(p) => (
|
path="/session/:id?"
|
||||||
<Show when={p.params.id || true} keyed>
|
component={(p) => (
|
||||||
<TerminalProvider>
|
<Show when={p.params.id ?? "new"} keyed>
|
||||||
<PromptProvider>
|
<TerminalProvider>
|
||||||
<Session />
|
<PromptProvider>
|
||||||
</PromptProvider>
|
<Session />
|
||||||
</TerminalProvider>
|
</PromptProvider>
|
||||||
</Show>
|
</TerminalProvider>
|
||||||
)}
|
</Show>
|
||||||
/>
|
)}
|
||||||
</Route>
|
/>
|
||||||
</Router>
|
</Route>
|
||||||
</NotificationProvider>
|
</Router>
|
||||||
</LayoutProvider>
|
</NotificationProvider>
|
||||||
</GlobalSyncProvider>
|
</LayoutProvider>
|
||||||
</GlobalSDKProvider>
|
</GlobalSyncProvider>
|
||||||
</CodeComponentProvider>
|
</GlobalSDKProvider>
|
||||||
</DiffComponentProvider>
|
</CodeComponentProvider>
|
||||||
</MarkedProvider>
|
</DiffComponentProvider>
|
||||||
</DialogProvider>
|
</MarkedProvider>
|
||||||
</ErrorBoundary>
|
</DialogProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</ThemeProvider>
|
||||||
</MetaProvider>
|
</MetaProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
|
import { TextField } from "@opencode-ai/ui/text-field"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { createMemo, createSignal, For, Show } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
|
import { type LocalProject, getAvatarColors } from "@/context/layout"
|
||||||
|
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||||
|
|
||||||
|
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
||||||
|
|
||||||
|
function getFilename(input: string) {
|
||||||
|
const parts = input.split("/")
|
||||||
|
return parts[parts.length - 1] || input
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DialogEditProject(props: { project: LocalProject }) {
|
||||||
|
const dialog = useDialog()
|
||||||
|
const globalSDK = useGlobalSDK()
|
||||||
|
|
||||||
|
const folderName = createMemo(() => getFilename(props.project.worktree))
|
||||||
|
const defaultName = createMemo(() => props.project.name || folderName())
|
||||||
|
|
||||||
|
const [store, setStore] = createStore({
|
||||||
|
name: defaultName(),
|
||||||
|
color: props.project.icon?.color || "pink",
|
||||||
|
iconUrl: props.project.icon?.url || "",
|
||||||
|
saving: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [dragOver, setDragOver] = createSignal(false)
|
||||||
|
|
||||||
|
function handleFileSelect(file: File) {
|
||||||
|
if (!file.type.startsWith("image/")) return
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => setStore("iconUrl", e.target?.result as string)
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDrop(e: DragEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
setDragOver(false)
|
||||||
|
const file = e.dataTransfer?.files[0]
|
||||||
|
if (file) handleFileSelect(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragOver(e: DragEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
setDragOver(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragLeave() {
|
||||||
|
setDragOver(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInputChange(e: Event) {
|
||||||
|
const input = e.target as HTMLInputElement
|
||||||
|
const file = input.files?.[0]
|
||||||
|
if (file) handleFileSelect(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearIcon() {
|
||||||
|
setStore("iconUrl", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit(e: SubmitEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!props.project.id) return
|
||||||
|
|
||||||
|
setStore("saving", true)
|
||||||
|
const name = store.name.trim() === folderName() ? "" : store.name.trim()
|
||||||
|
await globalSDK.client.project.update({
|
||||||
|
projectID: props.project.id,
|
||||||
|
name,
|
||||||
|
icon: { color: store.color, url: store.iconUrl },
|
||||||
|
})
|
||||||
|
setStore("saving", false)
|
||||||
|
dialog.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog title="Edit project">
|
||||||
|
<form onSubmit={handleSubmit} class="flex flex-col gap-6 px-2.5 pb-3">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<TextField
|
||||||
|
autofocus
|
||||||
|
type="text"
|
||||||
|
label="Name"
|
||||||
|
placeholder={folderName()}
|
||||||
|
value={store.name}
|
||||||
|
onChange={(v) => setStore("name", v)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-12-medium text-text-weak">Icon</label>
|
||||||
|
<div class="flex gap-3 items-start">
|
||||||
|
<div class="relative">
|
||||||
|
<div
|
||||||
|
class="size-16 rounded-lg overflow-hidden border border-dashed transition-colors cursor-pointer"
|
||||||
|
classList={{
|
||||||
|
"border-text-interactive-base bg-surface-info-base/20": dragOver(),
|
||||||
|
"border-border-base hover:border-border-strong": !dragOver(),
|
||||||
|
}}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onClick={() => document.getElementById("icon-upload")?.click()}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={store.iconUrl}
|
||||||
|
fallback={
|
||||||
|
<div class="size-full flex items-center justify-center">
|
||||||
|
<Avatar
|
||||||
|
fallback={store.name || defaultName()}
|
||||||
|
{...getAvatarColors(store.color)}
|
||||||
|
class="size-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<img src={store.iconUrl} alt="Project icon" class="size-full object-cover" />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<Show when={store.iconUrl}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute -top-1.5 -right-1.5 size-5 rounded-full bg-surface-raised-base border border-border-base flex items-center justify-center hover:bg-surface-raised-base-hover"
|
||||||
|
onClick={clearIcon}
|
||||||
|
>
|
||||||
|
<Icon name="close" class="size-3 text-icon-base" />
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<input id="icon-upload" type="file" accept="image/*" class="hidden" onChange={handleInputChange} />
|
||||||
|
<div class="flex flex-col gap-1.5 text-12-regular text-text-weak">
|
||||||
|
<span>Click or drag an image</span>
|
||||||
|
<span>Recommended: 128x128px</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={!store.iconUrl}>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-12-medium text-text-weak">Color</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<For each={AVATAR_COLOR_KEYS}>
|
||||||
|
{(color) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="relative size-8 rounded-md transition-all"
|
||||||
|
classList={{
|
||||||
|
"ring-2 ring-offset-2 ring-offset-surface-base ring-text-interactive-base":
|
||||||
|
store.color === color,
|
||||||
|
}}
|
||||||
|
style={{ background: getAvatarColors(color).background }}
|
||||||
|
onClick={() => setStore("color", color)}
|
||||||
|
>
|
||||||
|
<Avatar fallback={store.name || defaultName()} {...getAvatarColors(color)} class="size-full" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<Button type="button" variant="ghost" size="large" onClick={() => dialog.close()}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" variant="primary" size="large" disabled={store.saving}>
|
||||||
|
{store.saving ? "Saving..." : "Save"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { Component, createMemo, createSignal, Show } from "solid-js"
|
||||||
|
import { useSync } from "@/context/sync"
|
||||||
|
import { useSDK } from "@/context/sdk"
|
||||||
|
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||||
|
import { List } from "@opencode-ai/ui/list"
|
||||||
|
import { Switch } from "@opencode-ai/ui/switch"
|
||||||
|
|
||||||
|
export const DialogSelectMcp: Component = () => {
|
||||||
|
const sync = useSync()
|
||||||
|
const sdk = useSDK()
|
||||||
|
const [loading, setLoading] = createSignal<string | null>(null)
|
||||||
|
|
||||||
|
const items = createMemo(() =>
|
||||||
|
Object.entries(sync.data.mcp ?? {})
|
||||||
|
.map(([name, status]) => ({ name, status: status.status }))
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||||
|
)
|
||||||
|
|
||||||
|
const toggle = async (name: string) => {
|
||||||
|
if (loading()) return
|
||||||
|
setLoading(name)
|
||||||
|
const status = sync.data.mcp[name]
|
||||||
|
if (status?.status === "connected") {
|
||||||
|
await sdk.client.mcp.disconnect({ name })
|
||||||
|
} else {
|
||||||
|
await sdk.client.mcp.connect({ name })
|
||||||
|
}
|
||||||
|
const result = await sdk.client.mcp.status()
|
||||||
|
if (result.data) sync.set("mcp", result.data)
|
||||||
|
setLoading(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)
|
||||||
|
const totalCount = createMemo(() => items().length)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog title="MCPs" description={`${enabledCount()} of ${totalCount()} enabled`}>
|
||||||
|
<List
|
||||||
|
search={{ placeholder: "Search", autofocus: true }}
|
||||||
|
emptyMessage="No MCPs configured"
|
||||||
|
key={(x) => x?.name ?? ""}
|
||||||
|
items={items}
|
||||||
|
filterKeys={["name", "status"]}
|
||||||
|
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
||||||
|
onSelect={(x) => {
|
||||||
|
if (x) toggle(x.name)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(i) => {
|
||||||
|
const mcpStatus = () => sync.data.mcp[i.name]
|
||||||
|
const status = () => mcpStatus()?.status
|
||||||
|
const error = () => {
|
||||||
|
const s = mcpStatus()
|
||||||
|
return s?.status === "failed" ? s.error : undefined
|
||||||
|
}
|
||||||
|
const enabled = () => status() === "connected"
|
||||||
|
return (
|
||||||
|
<div class="w-full flex items-center justify-between gap-x-3">
|
||||||
|
<div class="flex flex-col gap-0.5 min-w-0">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="truncate">{i.name}</span>
|
||||||
|
<Show when={status() === "connected"}>
|
||||||
|
<span class="text-11-regular text-text-weaker">connected</span>
|
||||||
|
</Show>
|
||||||
|
<Show when={status() === "failed"}>
|
||||||
|
<span class="text-11-regular text-text-weaker">failed</span>
|
||||||
|
</Show>
|
||||||
|
<Show when={status() === "needs_auth"}>
|
||||||
|
<span class="text-11-regular text-text-weaker">needs auth</span>
|
||||||
|
</Show>
|
||||||
|
<Show when={status() === "disabled"}>
|
||||||
|
<span class="text-11-regular text-text-weaker">disabled</span>
|
||||||
|
</Show>
|
||||||
|
<Show when={loading() === i.name}>
|
||||||
|
<span class="text-11-regular text-text-weak">...</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<Show when={error()}>
|
||||||
|
<span class="text-11-regular text-text-weaker truncate">{error()}</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
|
<Switch checked={enabled()} disabled={loading() === i.name} onChange={() => toggle(i.name)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</List>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -188,6 +188,10 @@ export function Header(props: {
|
|||||||
shareURL = await globalSDK.client.session
|
shareURL = await globalSDK.client.session
|
||||||
.share({ sessionID: session.id, directory: currentDirectory() })
|
.share({ sessionID: session.id, directory: currentDirectory() })
|
||||||
.then((r) => r.data?.share?.url)
|
.then((r) => r.data?.share?.url)
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("Failed to share session", e)
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return shareURL
|
return shareURL
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -82,6 +82,37 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
let editorRef!: HTMLDivElement
|
let editorRef!: HTMLDivElement
|
||||||
let fileInputRef!: HTMLInputElement
|
let fileInputRef!: HTMLInputElement
|
||||||
|
let scrollRef!: HTMLDivElement
|
||||||
|
|
||||||
|
const scrollCursorIntoView = () => {
|
||||||
|
const container = scrollRef
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (!container || !selection || selection.rangeCount === 0) return
|
||||||
|
|
||||||
|
const range = selection.getRangeAt(0)
|
||||||
|
if (!editorRef.contains(range.startContainer)) return
|
||||||
|
|
||||||
|
const rect = range.getBoundingClientRect()
|
||||||
|
if (!rect.height) return
|
||||||
|
|
||||||
|
const containerRect = container.getBoundingClientRect()
|
||||||
|
const top = rect.top - containerRect.top + container.scrollTop
|
||||||
|
const bottom = rect.bottom - containerRect.top + container.scrollTop
|
||||||
|
const padding = 12
|
||||||
|
|
||||||
|
if (top < container.scrollTop + padding) {
|
||||||
|
container.scrollTop = Math.max(0, top - padding)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bottom > container.scrollTop + container.clientHeight - padding) {
|
||||||
|
container.scrollTop = bottom - container.clientHeight + padding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const queueScroll = () => {
|
||||||
|
requestAnimationFrame(scrollCursorIntoView)
|
||||||
|
}
|
||||||
|
|
||||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
||||||
@@ -103,7 +134,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
imageAttachments: ImageAttachmentPart[]
|
imageAttachments: ImageAttachmentPart[]
|
||||||
mode: "normal" | "shell"
|
mode: "normal" | "shell"
|
||||||
applyingHistory: boolean
|
applyingHistory: boolean
|
||||||
userHasEdited: boolean
|
|
||||||
}>({
|
}>({
|
||||||
popover: null,
|
popover: null,
|
||||||
historyIndex: -1,
|
historyIndex: -1,
|
||||||
@@ -113,7 +143,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
imageAttachments: [],
|
imageAttachments: [],
|
||||||
mode: "normal",
|
mode: "normal",
|
||||||
applyingHistory: false,
|
applyingHistory: false,
|
||||||
userHasEdited: false,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const MAX_HISTORY = 100
|
const MAX_HISTORY = 100
|
||||||
@@ -150,12 +179,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const applyHistoryPrompt = (p: Prompt, position: "start" | "end") => {
|
const applyHistoryPrompt = (p: Prompt, position: "start" | "end") => {
|
||||||
const length = position === "start" ? 0 : promptLength(p)
|
const length = position === "start" ? 0 : promptLength(p)
|
||||||
setStore("applyingHistory", true)
|
setStore("applyingHistory", true)
|
||||||
setStore("userHasEdited", false)
|
|
||||||
prompt.set(p, length)
|
prompt.set(p, length)
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
editorRef.focus()
|
editorRef.focus()
|
||||||
setCursorPosition(editorRef, length)
|
setCursorPosition(editorRef, length)
|
||||||
setStore("applyingHistory", false)
|
setStore("applyingHistory", false)
|
||||||
|
queueScroll()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,6 +248,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handlePaste = async (event: ClipboardEvent) => {
|
const handlePaste = async (event: ClipboardEvent) => {
|
||||||
|
if (!isFocused()) return
|
||||||
const clipboardData = event.clipboardData
|
const clipboardData = event.clipboardData
|
||||||
if (!clipboardData) return
|
if (!clipboardData) return
|
||||||
|
|
||||||
@@ -241,7 +271,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
addPart({ type: "text", content: plainText, start: 0, end: 0 })
|
addPart({ type: "text", content: plainText, start: 0, end: 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDragOver = (event: DragEvent) => {
|
const handleGlobalDragOver = (event: DragEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
const hasFiles = event.dataTransfer?.types.includes("Files")
|
const hasFiles = event.dataTransfer?.types.includes("Files")
|
||||||
if (hasFiles) {
|
if (hasFiles) {
|
||||||
@@ -249,15 +279,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDragLeave = (event: DragEvent) => {
|
const handleGlobalDragLeave = (event: DragEvent) => {
|
||||||
const related = event.relatedTarget as Node | null
|
// relatedTarget is null when leaving the document window
|
||||||
const form = event.currentTarget as HTMLElement
|
if (!event.relatedTarget) {
|
||||||
if (!related || !form.contains(related)) {
|
|
||||||
setStore("dragging", false)
|
setStore("dragging", false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDrop = async (event: DragEvent) => {
|
const handleGlobalDrop = async (event: DragEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
setStore("dragging", false)
|
setStore("dragging", false)
|
||||||
|
|
||||||
@@ -273,17 +302,19 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
editorRef.addEventListener("paste", handlePaste)
|
editorRef.addEventListener("paste", handlePaste)
|
||||||
|
document.addEventListener("dragover", handleGlobalDragOver)
|
||||||
|
document.addEventListener("dragleave", handleGlobalDragLeave)
|
||||||
|
document.addEventListener("drop", handleGlobalDrop)
|
||||||
})
|
})
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
editorRef.removeEventListener("paste", handlePaste)
|
editorRef.removeEventListener("paste", handlePaste)
|
||||||
|
document.removeEventListener("dragover", handleGlobalDragOver)
|
||||||
|
document.removeEventListener("dragleave", handleGlobalDragLeave)
|
||||||
|
document.removeEventListener("drop", handleGlobalDrop)
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (isFocused()) {
|
if (!isFocused()) setStore("popover", null)
|
||||||
handleInput()
|
|
||||||
} else {
|
|
||||||
setStore("popover", null)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleFileSelect = (path: string | undefined) => {
|
const handleFileSelect = (path: string | undefined) => {
|
||||||
@@ -363,7 +394,26 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
() => prompt.current(),
|
() => prompt.current(),
|
||||||
(currentParts) => {
|
(currentParts) => {
|
||||||
const domParts = parseFromDOM()
|
const domParts = parseFromDOM()
|
||||||
if (isPromptEqual(currentParts, domParts)) return
|
const normalized = Array.from(editorRef.childNodes).every((node) => {
|
||||||
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
|
const text = node.textContent ?? ""
|
||||||
|
if (!text.includes("\u200B")) return true
|
||||||
|
if (text !== "\u200B") return false
|
||||||
|
|
||||||
|
const prev = node.previousSibling
|
||||||
|
const next = node.nextSibling
|
||||||
|
const prevIsBr = prev?.nodeType === Node.ELEMENT_NODE && (prev as HTMLElement).tagName === "BR"
|
||||||
|
const nextIsBr = next?.nodeType === Node.ELEMENT_NODE && (next as HTMLElement).tagName === "BR"
|
||||||
|
if (!prevIsBr && !nextIsBr) return false
|
||||||
|
if (nextIsBr && !prevIsBr && prev) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (node.nodeType !== Node.ELEMENT_NODE) return false
|
||||||
|
const el = node as HTMLElement
|
||||||
|
if (el.dataset.type === "file") return true
|
||||||
|
return el.tagName === "BR"
|
||||||
|
})
|
||||||
|
if (normalized && isPromptEqual(currentParts, domParts)) return
|
||||||
|
|
||||||
const selection = window.getSelection()
|
const selection = window.getSelection()
|
||||||
let cursorPosition: number | null = null
|
let cursorPosition: number | null = null
|
||||||
@@ -374,7 +424,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
editorRef.innerHTML = ""
|
editorRef.innerHTML = ""
|
||||||
currentParts.forEach((part) => {
|
currentParts.forEach((part) => {
|
||||||
if (part.type === "text") {
|
if (part.type === "text") {
|
||||||
editorRef.appendChild(document.createTextNode(part.content))
|
editorRef.appendChild(createTextFragment(part.content))
|
||||||
} else if (part.type === "file") {
|
} else if (part.type === "file") {
|
||||||
const pill = document.createElement("span")
|
const pill = document.createElement("span")
|
||||||
pill.textContent = part.content
|
pill.textContent = part.content
|
||||||
@@ -395,34 +445,21 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const parseFromDOM = (): Prompt => {
|
const parseFromDOM = (): Prompt => {
|
||||||
const newParts: Prompt = []
|
const parts: Prompt = []
|
||||||
let position = 0
|
let position = 0
|
||||||
|
let buffer = ""
|
||||||
|
|
||||||
const pushText = (content: string) => {
|
const flushText = () => {
|
||||||
|
const content = buffer.replace(/\r\n?/g, "\n").replace(/\u200B/g, "")
|
||||||
|
buffer = ""
|
||||||
if (!content) return
|
if (!content) return
|
||||||
newParts.push({ type: "text", content, start: position, end: position + content.length })
|
parts.push({ type: "text", content, start: position, end: position + content.length })
|
||||||
position += content.length
|
position += content.length
|
||||||
}
|
}
|
||||||
|
|
||||||
const rangeText = (range: Range) => {
|
const pushFile = (file: HTMLElement) => {
|
||||||
const fragment = range.cloneContents()
|
|
||||||
const container = document.createElement("div")
|
|
||||||
container.append(fragment)
|
|
||||||
return container.innerText
|
|
||||||
}
|
|
||||||
|
|
||||||
const files = Array.from(editorRef.querySelectorAll<HTMLElement>("[data-type=file]"))
|
|
||||||
let last: HTMLElement | undefined
|
|
||||||
|
|
||||||
files.forEach((file) => {
|
|
||||||
const before = document.createRange()
|
|
||||||
before.selectNodeContents(editorRef)
|
|
||||||
if (last) before.setStartAfter(last)
|
|
||||||
before.setEndBefore(file)
|
|
||||||
pushText(rangeText(before))
|
|
||||||
|
|
||||||
const content = file.textContent ?? ""
|
const content = file.textContent ?? ""
|
||||||
newParts.push({
|
parts.push({
|
||||||
type: "file",
|
type: "file",
|
||||||
path: file.dataset.path!,
|
path: file.dataset.path!,
|
||||||
content,
|
content,
|
||||||
@@ -430,16 +467,44 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
end: position + content.length,
|
end: position + content.length,
|
||||||
})
|
})
|
||||||
position += content.length
|
position += content.length
|
||||||
last = file
|
}
|
||||||
|
|
||||||
|
const visit = (node: Node) => {
|
||||||
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
|
buffer += node.textContent ?? ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (node.nodeType !== Node.ELEMENT_NODE) return
|
||||||
|
|
||||||
|
const el = node as HTMLElement
|
||||||
|
if (el.dataset.type === "file") {
|
||||||
|
flushText()
|
||||||
|
pushFile(el)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (el.tagName === "BR") {
|
||||||
|
buffer += "\n"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const child of Array.from(el.childNodes)) {
|
||||||
|
visit(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = Array.from(editorRef.childNodes)
|
||||||
|
children.forEach((child, index) => {
|
||||||
|
const isBlock = child.nodeType === Node.ELEMENT_NODE && ["DIV", "P"].includes((child as HTMLElement).tagName)
|
||||||
|
visit(child)
|
||||||
|
if (isBlock && index < children.length - 1) {
|
||||||
|
buffer += "\n"
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const after = document.createRange()
|
flushText()
|
||||||
after.selectNodeContents(editorRef)
|
|
||||||
if (last) after.setStartAfter(last)
|
|
||||||
pushText(rangeText(after))
|
|
||||||
|
|
||||||
if (newParts.length === 0) newParts.push(...DEFAULT_PROMPT)
|
if (parts.length === 0) parts.push(...DEFAULT_PROMPT)
|
||||||
return newParts
|
return parts
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleInput = () => {
|
const handleInput = () => {
|
||||||
@@ -452,7 +517,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
|
|
||||||
if (shouldReset) {
|
if (shouldReset) {
|
||||||
setStore("popover", null)
|
setStore("popover", null)
|
||||||
setStore("userHasEdited", false)
|
|
||||||
if (store.historyIndex >= 0 && !store.applyingHistory) {
|
if (store.historyIndex >= 0 && !store.applyingHistory) {
|
||||||
setStore("historyIndex", -1)
|
setStore("historyIndex", -1)
|
||||||
setStore("savedPrompt", null)
|
setStore("savedPrompt", null)
|
||||||
@@ -460,6 +524,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
if (prompt.dirty()) {
|
if (prompt.dirty()) {
|
||||||
prompt.set(DEFAULT_PROMPT, 0)
|
prompt.set(DEFAULT_PROMPT, 0)
|
||||||
}
|
}
|
||||||
|
queueScroll()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,11 +552,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
setStore("savedPrompt", null)
|
setStore("savedPrompt", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!store.applyingHistory) {
|
|
||||||
setStore("userHasEdited", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
prompt.set(rawParts, cursorPosition)
|
prompt.set(rawParts, cursorPosition)
|
||||||
|
queueScroll()
|
||||||
}
|
}
|
||||||
|
|
||||||
const addPart = (part: ContentPart) => {
|
const addPart = (part: ContentPart) => {
|
||||||
@@ -516,27 +578,40 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const gap = document.createTextNode(" ")
|
const gap = document.createTextNode(" ")
|
||||||
const range = selection.getRangeAt(0)
|
const range = selection.getRangeAt(0)
|
||||||
|
|
||||||
if (atMatch) {
|
const setEdge = (edge: "start" | "end", offset: number) => {
|
||||||
let runningLength = 0
|
let remaining = offset
|
||||||
|
const nodes = Array.from(editorRef.childNodes)
|
||||||
|
|
||||||
const walker = document.createTreeWalker(editorRef, NodeFilter.SHOW_TEXT, null)
|
for (const node of nodes) {
|
||||||
let currentNode = walker.nextNode()
|
const length = getNodeLength(node)
|
||||||
while (currentNode) {
|
const isText = node.nodeType === Node.TEXT_NODE
|
||||||
const textContent = currentNode.textContent || ""
|
const isFile = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).dataset.type === "file"
|
||||||
if (runningLength + textContent.length >= atMatch.index!) {
|
const isBreak = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR"
|
||||||
const localStart = atMatch.index! - runningLength
|
|
||||||
const localEnd = cursorPosition - runningLength
|
if (isText && remaining <= length) {
|
||||||
if (currentNode === range.startContainer || runningLength + textContent.length >= cursorPosition) {
|
if (edge === "start") range.setStart(node, remaining)
|
||||||
range.setStart(currentNode, localStart)
|
if (edge === "end") range.setEnd(node, remaining)
|
||||||
range.setEnd(currentNode, Math.min(localEnd, textContent.length))
|
return
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
runningLength += textContent.length
|
|
||||||
currentNode = walker.nextNode()
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (atMatch) {
|
||||||
|
const start = atMatch.index ?? cursorPosition - atMatch[0].length
|
||||||
|
setEdge("start", start)
|
||||||
|
setEdge("end", cursorPosition)
|
||||||
|
}
|
||||||
|
|
||||||
range.deleteContents()
|
range.deleteContents()
|
||||||
range.insertNode(gap)
|
range.insertNode(gap)
|
||||||
range.insertNode(pill)
|
range.insertNode(pill)
|
||||||
@@ -545,11 +620,25 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
selection.removeAllRanges()
|
selection.removeAllRanges()
|
||||||
selection.addRange(range)
|
selection.addRange(range)
|
||||||
} else if (part.type === "text") {
|
} else if (part.type === "text") {
|
||||||
const textNode = document.createTextNode(part.content)
|
|
||||||
const range = selection.getRangeAt(0)
|
const range = selection.getRangeAt(0)
|
||||||
|
const fragment = createTextFragment(part.content)
|
||||||
|
const last = fragment.lastChild
|
||||||
range.deleteContents()
|
range.deleteContents()
|
||||||
range.insertNode(textNode)
|
range.insertNode(fragment)
|
||||||
range.setStartAfter(textNode)
|
if (last) {
|
||||||
|
if (last.nodeType === Node.TEXT_NODE) {
|
||||||
|
const text = last.textContent ?? ""
|
||||||
|
if (text === "\u200B") {
|
||||||
|
range.setStart(last, 0)
|
||||||
|
}
|
||||||
|
if (text !== "\u200B") {
|
||||||
|
range.setStart(last, text.length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (last.nodeType !== Node.TEXT_NODE) {
|
||||||
|
range.setStartAfter(last)
|
||||||
|
}
|
||||||
|
}
|
||||||
range.collapse(true)
|
range.collapse(true)
|
||||||
selection.removeAllRanges()
|
selection.removeAllRanges()
|
||||||
selection.addRange(range)
|
selection.addRange(range)
|
||||||
@@ -560,9 +649,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const abort = () =>
|
const abort = () =>
|
||||||
sdk.client.session.abort({
|
sdk.client.session
|
||||||
sessionID: params.id!,
|
.abort({
|
||||||
})
|
sessionID: params.id!,
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
|
||||||
const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => {
|
const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => {
|
||||||
const text = prompt
|
const text = prompt
|
||||||
@@ -584,8 +675,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const navigateHistory = (direction: "up" | "down") => {
|
const navigateHistory = (direction: "up" | "down") => {
|
||||||
if (store.userHasEdited) return false
|
|
||||||
|
|
||||||
const entries = store.mode === "shell" ? shellHistory.entries : history.entries
|
const entries = store.mode === "shell" ? shellHistory.entries : history.entries
|
||||||
const current = store.historyIndex
|
const current = store.historyIndex
|
||||||
|
|
||||||
@@ -628,6 +717,24 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "Backspace") {
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (selection && selection.isCollapsed) {
|
||||||
|
const node = selection.anchorNode
|
||||||
|
const offset = selection.anchorOffset
|
||||||
|
if (node && node.nodeType === Node.TEXT_NODE) {
|
||||||
|
const text = node.textContent ?? ""
|
||||||
|
if (/^\u200B+$/.test(text) && offset > 0) {
|
||||||
|
const range = document.createRange()
|
||||||
|
range.setStart(node, 0)
|
||||||
|
range.collapse(true)
|
||||||
|
selection.removeAllRanges()
|
||||||
|
selection.addRange(range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (event.key === "!" && store.mode === "normal") {
|
if (event.key === "!" && store.mode === "normal") {
|
||||||
const cursorPosition = getCursorPosition(editorRef)
|
const cursorPosition = getCursorPosition(editorRef)
|
||||||
if (cursorPosition === 0) {
|
if (cursorPosition === 0) {
|
||||||
@@ -668,7 +775,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
|
|
||||||
const cursorPosition = getCursorPosition(editorRef)
|
const cursorPosition = getCursorPosition(editorRef)
|
||||||
const textLength = promptLength(prompt.current())
|
const textLength = promptLength(prompt.current())
|
||||||
const textContent = editorRef.textContent ?? ""
|
const textContent = prompt
|
||||||
|
.current()
|
||||||
|
.map((part) => ("content" in part ? part.content : ""))
|
||||||
|
.join("")
|
||||||
const isEmpty = textContent.trim() === "" || textLength <= 1
|
const isEmpty = textContent.trim() === "" || textLength <= 1
|
||||||
const hasNewlines = textContent.includes("\n")
|
const hasNewlines = textContent.includes("\n")
|
||||||
const inHistory = store.historyIndex >= 0
|
const inHistory = store.historyIndex >= 0
|
||||||
@@ -692,6 +802,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.key === "Enter" && event.shiftKey) {
|
||||||
|
addPart({ type: "text", content: "\n", start: 0, end: 0 })
|
||||||
|
event.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (event.key === "Enter" && !event.shiftKey) {
|
if (event.key === "Enter" && !event.shiftKey) {
|
||||||
handleSubmit(event)
|
handleSubmit(event)
|
||||||
}
|
}
|
||||||
@@ -717,7 +832,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
addToHistory(currentPrompt, store.mode)
|
addToHistory(currentPrompt, store.mode)
|
||||||
setStore("historyIndex", -1)
|
setStore("historyIndex", -1)
|
||||||
setStore("savedPrompt", null)
|
setStore("savedPrompt", null)
|
||||||
setStore("userHasEdited", false)
|
|
||||||
|
|
||||||
let existing = info()
|
let existing = info()
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
@@ -777,12 +891,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const agent = local.agent.current()!.name
|
const agent = local.agent.current()!.name
|
||||||
|
|
||||||
if (isShellMode) {
|
if (isShellMode) {
|
||||||
sdk.client.session.shell({
|
sdk.client.session
|
||||||
sessionID: existing.id,
|
.shell({
|
||||||
agent,
|
sessionID: existing.id,
|
||||||
model,
|
agent,
|
||||||
command: text,
|
model,
|
||||||
})
|
command: text,
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("Failed to send shell command", e)
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -791,13 +909,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const commandName = cmdName.slice(1)
|
const commandName = cmdName.slice(1)
|
||||||
const customCommand = sync.data.command.find((c) => c.name === commandName)
|
const customCommand = sync.data.command.find((c) => c.name === commandName)
|
||||||
if (customCommand) {
|
if (customCommand) {
|
||||||
sdk.client.session.command({
|
sdk.client.session
|
||||||
sessionID: existing.id,
|
.command({
|
||||||
command: commandName,
|
sessionID: existing.id,
|
||||||
arguments: args.join(" "),
|
command: commandName,
|
||||||
agent,
|
arguments: args.join(" "),
|
||||||
model: `${model.providerID}/${model.modelID}`,
|
agent,
|
||||||
})
|
model: `${model.providerID}/${model.modelID}`,
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("Failed to send command", e)
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -823,13 +945,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
model,
|
model,
|
||||||
})
|
})
|
||||||
|
|
||||||
sdk.client.session.prompt({
|
sdk.client.session
|
||||||
sessionID: existing.id,
|
.prompt({
|
||||||
agent,
|
sessionID: existing.id,
|
||||||
model,
|
agent,
|
||||||
messageID,
|
model,
|
||||||
parts: requestParts,
|
messageID,
|
||||||
})
|
parts: requestParts,
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("Failed to send prompt", e)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -904,9 +1030,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
</Show>
|
</Show>
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onDragOver={handleDragOver}
|
|
||||||
onDragLeave={handleDragLeave}
|
|
||||||
onDrop={handleDrop}
|
|
||||||
classList={{
|
classList={{
|
||||||
"bg-surface-raised-stronger-non-alpha shadow-xs-border relative": true,
|
"bg-surface-raised-stronger-non-alpha shadow-xs-border relative": true,
|
||||||
"rounded-md overflow-clip focus-within:shadow-xs-border": true,
|
"rounded-md overflow-clip focus-within:shadow-xs-border": true,
|
||||||
@@ -956,7 +1079,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class="relative max-h-[240px] overflow-y-auto">
|
<div class="relative max-h-[240px] overflow-y-auto" ref={(el) => (scrollRef = el)}>
|
||||||
<div
|
<div
|
||||||
data-component="prompt-input"
|
data-component="prompt-input"
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
@@ -967,18 +1090,21 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
onInput={handleInput}
|
onInput={handleInput}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
classList={{
|
classList={{
|
||||||
"w-full px-5 py-3 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",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Show when={!prompt.dirty() && store.imageAttachments.length === 0}>
|
<Show when={!prompt.dirty() && store.imageAttachments.length === 0}>
|
||||||
<div class="absolute top-0 inset-x-0 px-5 py-3 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate">
|
<div class="absolute top-0 inset-x-0 px-5 py-3 pr-12 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate">
|
||||||
{store.mode === "shell"
|
{store.mode === "shell"
|
||||||
? "Enter shell command..."
|
? "Enter shell command..."
|
||||||
: `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
|
: `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
<div class="absolute top-4.5 right-4">
|
||||||
|
<SessionContextUsage />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative p-3 flex items-center justify-between">
|
<div class="relative p-3 flex items-center justify-between">
|
||||||
<div class="flex items-center justify-start gap-1">
|
<div class="flex items-center justify-start gap-1">
|
||||||
@@ -1035,7 +1161,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
<SessionContextUsage />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1 absolute right-2 bottom-2">
|
<div class="flex items-center gap-1 absolute right-2 bottom-2">
|
||||||
<input
|
<input
|
||||||
@@ -1095,23 +1220,56 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createTextFragment(content: string): DocumentFragment {
|
||||||
|
const fragment = document.createDocumentFragment()
|
||||||
|
const segments = content.split("\n")
|
||||||
|
segments.forEach((segment, index) => {
|
||||||
|
if (segment) {
|
||||||
|
fragment.appendChild(document.createTextNode(segment))
|
||||||
|
} else if (segments.length > 1) {
|
||||||
|
fragment.appendChild(document.createTextNode("\u200B"))
|
||||||
|
}
|
||||||
|
if (index < segments.length - 1) {
|
||||||
|
fragment.appendChild(document.createElement("br"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeLength(node: Node): number {
|
||||||
|
if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR") return 1
|
||||||
|
return (node.textContent ?? "").replace(/\u200B/g, "").length
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTextLength(node: Node): number {
|
||||||
|
if (node.nodeType === Node.TEXT_NODE) return (node.textContent ?? "").replace(/\u200B/g, "").length
|
||||||
|
if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR") return 1
|
||||||
|
let length = 0
|
||||||
|
for (const child of Array.from(node.childNodes)) {
|
||||||
|
length += getTextLength(child)
|
||||||
|
}
|
||||||
|
return length
|
||||||
|
}
|
||||||
|
|
||||||
function getCursorPosition(parent: HTMLElement): number {
|
function getCursorPosition(parent: HTMLElement): number {
|
||||||
const selection = window.getSelection()
|
const selection = window.getSelection()
|
||||||
if (!selection || selection.rangeCount === 0) return 0
|
if (!selection || selection.rangeCount === 0) return 0
|
||||||
const range = selection.getRangeAt(0)
|
const range = selection.getRangeAt(0)
|
||||||
|
if (!parent.contains(range.startContainer)) return 0
|
||||||
const preCaretRange = range.cloneRange()
|
const preCaretRange = range.cloneRange()
|
||||||
preCaretRange.selectNodeContents(parent)
|
preCaretRange.selectNodeContents(parent)
|
||||||
preCaretRange.setEnd(range.startContainer, range.startOffset)
|
preCaretRange.setEnd(range.startContainer, range.startOffset)
|
||||||
return preCaretRange.toString().length
|
return getTextLength(preCaretRange.cloneContents())
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCursorPosition(parent: HTMLElement, position: number) {
|
function setCursorPosition(parent: HTMLElement, position: number) {
|
||||||
let remaining = position
|
let remaining = position
|
||||||
let node = parent.firstChild
|
let node = parent.firstChild
|
||||||
while (node) {
|
while (node) {
|
||||||
const length = node.textContent ? node.textContent.length : 0
|
const length = getNodeLength(node)
|
||||||
const isText = node.nodeType === Node.TEXT_NODE
|
const isText = node.nodeType === Node.TEXT_NODE
|
||||||
const isFile = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).dataset.type === "file"
|
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 (isText && remaining <= length) {
|
||||||
const range = document.createRange()
|
const range = document.createRange()
|
||||||
@@ -1123,10 +1281,24 @@ function setCursorPosition(parent: HTMLElement, position: number) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFile && remaining <= length) {
|
if ((isFile || isBreak) && remaining <= length) {
|
||||||
const range = document.createRange()
|
const range = document.createRange()
|
||||||
const selection = window.getSelection()
|
const selection = window.getSelection()
|
||||||
range.setStartAfter(node)
|
if (remaining === 0) {
|
||||||
|
range.setStartBefore(node)
|
||||||
|
}
|
||||||
|
if (remaining > 0 && isFile) {
|
||||||
|
range.setStartAfter(node)
|
||||||
|
}
|
||||||
|
if (remaining > 0 && isBreak) {
|
||||||
|
const next = node.nextSibling
|
||||||
|
if (next && next.nodeType === Node.TEXT_NODE) {
|
||||||
|
range.setStart(next, 0)
|
||||||
|
}
|
||||||
|
if (!next || next.nodeType !== Node.TEXT_NODE) {
|
||||||
|
range.setStartAfter(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
range.collapse(true)
|
range.collapse(true)
|
||||||
selection?.removeAllRanges()
|
selection?.removeAllRanges()
|
||||||
selection?.addRange(range)
|
selection?.addRange(range)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
|
|||||||
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
|
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { AssistantMessage } from "@opencode-ai/sdk/v2"
|
import { AssistantMessage } from "@opencode-ai/sdk/v2/client"
|
||||||
|
|
||||||
export function SessionContextUsage() {
|
export function SessionContextUsage() {
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
@@ -34,28 +34,21 @@ export function SessionContextUsage() {
|
|||||||
<Show when={context?.()}>
|
<Show when={context?.()}>
|
||||||
{(ctx) => (
|
{(ctx) => (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
openDelay={300}
|
|
||||||
value={
|
value={
|
||||||
<div class="flex flex-col gap-1 p-2">
|
<div class="grid grid-cols-2 gap-x-3 gap-y-1">
|
||||||
<div class="flex justify-between gap-4">
|
<span class="opacity-70 text-right">Tokens</span>
|
||||||
<span class="text-text-weaker">Tokens</span>
|
<span class="text-left">{ctx().tokens}</span>
|
||||||
<span class="text-text-strong">{ctx().tokens}</span>
|
<span class="opacity-70 text-right">Usage</span>
|
||||||
</div>
|
<span class="text-left">{ctx().percentage ?? 0}%</span>
|
||||||
<div class="flex justify-between gap-4">
|
<span class="opacity-70 text-right">Cost</span>
|
||||||
<span class="text-text-weaker">Usage</span>
|
<span class="text-left">{cost()}</span>
|
||||||
<span class="text-text-strong">{ctx().percentage ?? 0}%</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between gap-4">
|
|
||||||
<span class="text-text-weaker">Cost</span>
|
|
||||||
<span class="text-text-strong">{cost()}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1.5">
|
||||||
<span class="text-12-medium text-text-weak">{`${ctx().percentage ?? 0}%`}</span>
|
|
||||||
<ProgressCircle size={16} strokeWidth={2} percentage={ctx().percentage ?? 0} />
|
<ProgressCircle size={16} strokeWidth={2} percentage={ctx().percentage ?? 0} />
|
||||||
|
{/* <span class="text-12-medium text-text-weak">{`${ctx().percentage ?? 0}%`}</span> */}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { createMemo, Show } from "solid-js"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { useSync } from "@/context/sync"
|
||||||
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
|
|
||||||
|
export function SessionLspIndicator() {
|
||||||
|
const sync = useSync()
|
||||||
|
|
||||||
|
const lspStats = createMemo(() => {
|
||||||
|
const lsp = sync.data.lsp ?? []
|
||||||
|
const connected = lsp.filter((s) => s.status === "connected").length
|
||||||
|
const hasError = lsp.some((s) => s.status === "error")
|
||||||
|
const total = lsp.length
|
||||||
|
return { connected, hasError, total }
|
||||||
|
})
|
||||||
|
|
||||||
|
const tooltipContent = createMemo(() => {
|
||||||
|
const lsp = sync.data.lsp ?? []
|
||||||
|
if (lsp.length === 0) return "No LSP servers"
|
||||||
|
return lsp.map((s) => s.name).join(", ")
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={lspStats().total > 0}>
|
||||||
|
<Tooltip placement="top" value={tooltipContent()}>
|
||||||
|
<div class="flex items-center gap-1 px-2 cursor-default select-none">
|
||||||
|
<Icon
|
||||||
|
name="code"
|
||||||
|
size="small"
|
||||||
|
classList={{
|
||||||
|
"text-icon-critical-base": lspStats().hasError,
|
||||||
|
"text-icon-success-base": !lspStats().hasError && lspStats().connected > 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span class="text-12-regular text-text-weak">{lspStats().connected} LSP</span>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { createMemo, Show } from "solid-js"
|
||||||
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
import { useSync } from "@/context/sync"
|
||||||
|
import { DialogSelectMcp } from "@/components/dialog-select-mcp"
|
||||||
|
|
||||||
|
export function SessionMcpIndicator() {
|
||||||
|
const sync = useSync()
|
||||||
|
const dialog = useDialog()
|
||||||
|
|
||||||
|
const mcpStats = createMemo(() => {
|
||||||
|
const mcp = sync.data.mcp ?? {}
|
||||||
|
const entries = Object.entries(mcp)
|
||||||
|
const enabled = entries.filter(([, status]) => status.status === "connected").length
|
||||||
|
const failed = entries.some(([, status]) => status.status === "failed")
|
||||||
|
const total = entries.length
|
||||||
|
return { enabled, failed, total }
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={mcpStats().total > 0}>
|
||||||
|
<Button variant="ghost" onClick={() => dialog.show(() => <DialogSelectMcp />)}>
|
||||||
|
<Icon
|
||||||
|
name="mcp"
|
||||||
|
size="small"
|
||||||
|
classList={{
|
||||||
|
"text-icon-critical-base": mcpStats().failed,
|
||||||
|
"text-icon-success-base": !mcpStats().failed && mcpStats().enabled > 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span class="text-12-regular text-text-weak">{mcpStats().enabled} MCP</span>
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { createMemo, Show, type ParentProps } from "solid-js"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
|
import { useSync } from "@/context/sync"
|
||||||
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
|
|
||||||
|
export function StatusBar(props: ParentProps) {
|
||||||
|
const platform = usePlatform()
|
||||||
|
const sync = useSync()
|
||||||
|
const globalSync = useGlobalSync()
|
||||||
|
|
||||||
|
const directoryDisplay = createMemo(() => {
|
||||||
|
const directory = sync.data.path.directory || ""
|
||||||
|
const home = globalSync.data.path.home || ""
|
||||||
|
const short = home && directory.startsWith(home) ? directory.replace(home, "~") : directory
|
||||||
|
const branch = sync.data.vcs?.branch
|
||||||
|
return branch ? `${short}:${branch}` : short
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="h-8 w-full shrink-0 flex items-center justify-between px-2 border-t border-border-weak-base bg-background-base">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<Show when={platform.version}>
|
||||||
|
<span class="text-12-regular text-text-weak">v{platform.version}</span>
|
||||||
|
</Show>
|
||||||
|
<Show when={directoryDisplay()}>
|
||||||
|
<span class="text-12-regular text-text-weak">{directoryDisplay()}</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">{props.children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
|
import { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
|
||||||
import { ComponentProps, onCleanup, onMount, splitProps } from "solid-js"
|
import { ComponentProps, createEffect, createSignal, onCleanup, onMount, splitProps } from "solid-js"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { SerializeAddon } from "@/addons/serialize"
|
import { SerializeAddon } from "@/addons/serialize"
|
||||||
import { LocalPTY } from "@/context/terminal"
|
import { LocalPTY } from "@/context/terminal"
|
||||||
import { usePrefersDark } from "@solid-primitives/media"
|
import { resolveThemeVariant, useTheme } from "@opencode-ai/ui/theme"
|
||||||
|
|
||||||
export interface TerminalProps extends ComponentProps<"div"> {
|
export interface TerminalProps extends ComponentProps<"div"> {
|
||||||
pty: LocalPTY
|
pty: LocalPTY
|
||||||
@@ -12,8 +12,28 @@ export interface TerminalProps extends ComponentProps<"div"> {
|
|||||||
onConnectError?: (error: unknown) => void
|
onConnectError?: (error: unknown) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TerminalColors = {
|
||||||
|
background: string
|
||||||
|
foreground: string
|
||||||
|
cursor: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_TERMINAL_COLORS: Record<"light" | "dark", TerminalColors> = {
|
||||||
|
light: {
|
||||||
|
background: "#fcfcfc",
|
||||||
|
foreground: "#211e1e",
|
||||||
|
cursor: "#211e1e",
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
background: "#191515",
|
||||||
|
foreground: "#d4d4d4",
|
||||||
|
cursor: "#d4d4d4",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
export const Terminal = (props: TerminalProps) => {
|
export const Terminal = (props: TerminalProps) => {
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
const theme = useTheme()
|
||||||
let container!: HTMLDivElement
|
let container!: HTMLDivElement
|
||||||
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"])
|
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"])
|
||||||
let ws: WebSocket
|
let ws: WebSocket
|
||||||
@@ -22,7 +42,64 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
let serializeAddon: SerializeAddon
|
let serializeAddon: SerializeAddon
|
||||||
let fitAddon: FitAddon
|
let fitAddon: FitAddon
|
||||||
let handleResize: () => void
|
let handleResize: () => void
|
||||||
const prefersDark = usePrefersDark()
|
|
||||||
|
const getTerminalColors = (): TerminalColors => {
|
||||||
|
const mode = theme.mode()
|
||||||
|
const fallback = DEFAULT_TERMINAL_COLORS[mode]
|
||||||
|
const currentTheme = theme.themes()[theme.themeId()]
|
||||||
|
if (!currentTheme) return fallback
|
||||||
|
const variant = mode === "dark" ? currentTheme.dark : currentTheme.light
|
||||||
|
if (!variant?.seeds) return fallback
|
||||||
|
const resolved = resolveThemeVariant(variant, mode === "dark")
|
||||||
|
const text = resolved["text-base"] ?? fallback.foreground
|
||||||
|
const background = resolved["background-stronger"] ?? fallback.background
|
||||||
|
return {
|
||||||
|
background,
|
||||||
|
foreground: text,
|
||||||
|
cursor: text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [terminalColors, setTerminalColors] = createSignal<TerminalColors>(getTerminalColors())
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const colors = getTerminalColors()
|
||||||
|
setTerminalColors(colors)
|
||||||
|
if (!term) return
|
||||||
|
const setOption = (term as unknown as { setOption?: (key: string, value: TerminalColors) => void }).setOption
|
||||||
|
if (!setOption) return
|
||||||
|
setOption("theme", colors)
|
||||||
|
})
|
||||||
|
|
||||||
|
const focusTerminal = () => term?.focus()
|
||||||
|
const copySelection = () => {
|
||||||
|
if (!term || !term.hasSelection()) return false
|
||||||
|
const selection = term.getSelection()
|
||||||
|
if (!selection) return false
|
||||||
|
const clipboard = navigator.clipboard
|
||||||
|
if (clipboard?.writeText) {
|
||||||
|
clipboard.writeText(selection).catch(() => {})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (!document.body) return false
|
||||||
|
const textarea = document.createElement("textarea")
|
||||||
|
textarea.value = selection
|
||||||
|
textarea.setAttribute("readonly", "")
|
||||||
|
textarea.style.position = "fixed"
|
||||||
|
textarea.style.opacity = "0"
|
||||||
|
document.body.appendChild(textarea)
|
||||||
|
textarea.select()
|
||||||
|
const copied = document.execCommand("copy")
|
||||||
|
document.body.removeChild(textarea)
|
||||||
|
return copied
|
||||||
|
}
|
||||||
|
const handlePointerDown = () => {
|
||||||
|
const activeElement = document.activeElement
|
||||||
|
if (activeElement instanceof HTMLElement && activeElement !== container) {
|
||||||
|
activeElement.blur()
|
||||||
|
}
|
||||||
|
focusTerminal()
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
ghostty = await Ghostty.load()
|
ghostty = await Ghostty.load()
|
||||||
@@ -33,23 +110,22 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontFamily: "IBM Plex Mono, monospace",
|
fontFamily: "IBM Plex Mono, monospace",
|
||||||
allowTransparency: true,
|
allowTransparency: true,
|
||||||
theme: prefersDark()
|
theme: terminalColors(),
|
||||||
? {
|
|
||||||
background: "#191515",
|
|
||||||
foreground: "#d4d4d4",
|
|
||||||
cursor: "#d4d4d4",
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
background: "#fcfcfc",
|
|
||||||
foreground: "#211e1e",
|
|
||||||
cursor: "#211e1e",
|
|
||||||
},
|
|
||||||
scrollback: 10_000,
|
scrollback: 10_000,
|
||||||
ghostty,
|
ghostty,
|
||||||
})
|
})
|
||||||
term.attachCustomKeyEventHandler((event) => {
|
term.attachCustomKeyEventHandler((event) => {
|
||||||
|
const key = event.key.toLowerCase()
|
||||||
|
if (key === "c") {
|
||||||
|
const macCopy = event.metaKey && !event.ctrlKey && !event.altKey
|
||||||
|
const linuxCopy = event.ctrlKey && event.shiftKey && !event.metaKey
|
||||||
|
if ((macCopy || linuxCopy) && copySelection()) {
|
||||||
|
event.preventDefault()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
// allow for ctrl-` to toggle terminal in parent
|
// allow for ctrl-` to toggle terminal in parent
|
||||||
if (event.ctrlKey && event.key.toLowerCase() === "`") {
|
if (event.ctrlKey && key === "`") {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -62,6 +138,8 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
term.loadAddon(fitAddon)
|
term.loadAddon(fitAddon)
|
||||||
|
|
||||||
term.open(container)
|
term.open(container)
|
||||||
|
container.addEventListener("pointerdown", handlePointerDown)
|
||||||
|
focusTerminal()
|
||||||
|
|
||||||
if (local.pty.buffer) {
|
if (local.pty.buffer) {
|
||||||
if (local.pty.rows && local.pty.cols) {
|
if (local.pty.rows && local.pty.cols) {
|
||||||
@@ -75,20 +153,20 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
fitAddon.fit()
|
fitAddon.fit()
|
||||||
}
|
}
|
||||||
|
|
||||||
container.focus()
|
|
||||||
|
|
||||||
fitAddon.observeResize()
|
fitAddon.observeResize()
|
||||||
handleResize = () => fitAddon.fit()
|
handleResize = () => fitAddon.fit()
|
||||||
window.addEventListener("resize", handleResize)
|
window.addEventListener("resize", handleResize)
|
||||||
term.onResize(async (size) => {
|
term.onResize(async (size) => {
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
await sdk.client.pty.update({
|
await sdk.client.pty
|
||||||
ptyID: local.pty.id,
|
.update({
|
||||||
size: {
|
ptyID: local.pty.id,
|
||||||
cols: size.cols,
|
size: {
|
||||||
rows: size.rows,
|
cols: size.cols,
|
||||||
},
|
rows: size.rows,
|
||||||
})
|
},
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
term.onData((data) => {
|
term.onData((data) => {
|
||||||
@@ -106,13 +184,15 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
// })
|
// })
|
||||||
ws.addEventListener("open", () => {
|
ws.addEventListener("open", () => {
|
||||||
console.log("WebSocket connected")
|
console.log("WebSocket connected")
|
||||||
sdk.client.pty.update({
|
sdk.client.pty
|
||||||
ptyID: local.pty.id,
|
.update({
|
||||||
size: {
|
ptyID: local.pty.id,
|
||||||
cols: term.cols,
|
size: {
|
||||||
rows: term.rows,
|
cols: term.cols,
|
||||||
},
|
rows: term.rows,
|
||||||
})
|
},
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
})
|
})
|
||||||
ws.addEventListener("message", (event) => {
|
ws.addEventListener("message", (event) => {
|
||||||
term.write(event.data)
|
term.write(event.data)
|
||||||
@@ -130,6 +210,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
if (handleResize) {
|
if (handleResize) {
|
||||||
window.removeEventListener("resize", handleResize)
|
window.removeEventListener("resize", handleResize)
|
||||||
}
|
}
|
||||||
|
container.removeEventListener("pointerdown", handlePointerDown)
|
||||||
if (serializeAddon && props.onCleanup) {
|
if (serializeAddon && props.onCleanup) {
|
||||||
const buffer = serializeAddon.serialize()
|
const buffer = serializeAddon.serialize()
|
||||||
props.onCleanup({
|
props.onCleanup({
|
||||||
@@ -149,6 +230,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
ref={container}
|
ref={container}
|
||||||
data-component="terminal"
|
data-component="terminal"
|
||||||
data-prevent-autofocus
|
data-prevent-autofocus
|
||||||
|
style={{ "background-color": terminalColors().background }}
|
||||||
classList={{
|
classList={{
|
||||||
...(local.classList ?? {}),
|
...(local.classList ?? {}),
|
||||||
"size-full px-6 py-3 font-mono": true,
|
"size-full px-6 py-3 font-mono": true,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export interface CommandOption {
|
|||||||
suggested?: boolean
|
suggested?: boolean
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
onSelect?: (source?: "palette" | "keybind" | "slash") => void
|
onSelect?: (source?: "palette" | "keybind" | "slash") => void
|
||||||
|
onHighlight?: () => (() => void) | void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseKeybind(config: string): Keybind[] {
|
export function parseKeybind(config: string): Keybind[] {
|
||||||
@@ -115,6 +116,28 @@ export function formatKeybind(config: string): string {
|
|||||||
|
|
||||||
function DialogCommand(props: { options: CommandOption[] }) {
|
function DialogCommand(props: { options: CommandOption[] }) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
|
let cleanup: (() => void) | void
|
||||||
|
let committed = false
|
||||||
|
|
||||||
|
const handleMove = (option: CommandOption | undefined) => {
|
||||||
|
cleanup?.()
|
||||||
|
cleanup = option?.onHighlight?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelect = (option: CommandOption | undefined) => {
|
||||||
|
if (option) {
|
||||||
|
committed = true
|
||||||
|
cleanup = undefined
|
||||||
|
dialog.close()
|
||||||
|
option.onSelect?.("palette")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
if (!committed) {
|
||||||
|
cleanup?.()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog title="Commands">
|
<Dialog title="Commands">
|
||||||
@@ -125,12 +148,8 @@ function DialogCommand(props: { options: CommandOption[] }) {
|
|||||||
key={(x) => x?.id}
|
key={(x) => x?.id}
|
||||||
filterKeys={["title", "description", "category"]}
|
filterKeys={["title", "description", "category"]}
|
||||||
groupBy={(x) => x.category ?? ""}
|
groupBy={(x) => x.category ?? ""}
|
||||||
onSelect={(option) => {
|
onMove={handleMove}
|
||||||
if (option) {
|
onSelect={handleSelect}
|
||||||
dialog.close()
|
|
||||||
option.onSelect?.("palette")
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{(option) => (
|
{(option) => (
|
||||||
<div class="w-full flex items-center justify-between gap-4">
|
<div class="w-full flex items-center justify-between gap-4">
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ import {
|
|||||||
type ProviderListResponse,
|
type ProviderListResponse,
|
||||||
type ProviderAuthResponse,
|
type ProviderAuthResponse,
|
||||||
type Command,
|
type Command,
|
||||||
|
type McpStatus,
|
||||||
|
type LspStatus,
|
||||||
|
type VcsInfo,
|
||||||
|
type Permission,
|
||||||
createOpencodeClient,
|
createOpencodeClient,
|
||||||
} from "@opencode-ai/sdk/v2/client"
|
} from "@opencode-ai/sdk/v2/client"
|
||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
import { createStore, produce, reconcile } from "solid-js/store"
|
||||||
@@ -19,7 +23,7 @@ import { Binary } from "@opencode-ai/util/binary"
|
|||||||
import { retry } from "@opencode-ai/util/retry"
|
import { retry } from "@opencode-ai/util/retry"
|
||||||
import { useGlobalSDK } from "./global-sdk"
|
import { useGlobalSDK } from "./global-sdk"
|
||||||
import { ErrorPage, type InitError } from "../pages/error"
|
import { ErrorPage, type InitError } from "../pages/error"
|
||||||
import { createContext, useContext, onMount, type ParentProps, Switch, Match } from "solid-js"
|
import { batch, createContext, useContext, onMount, type ParentProps, Switch, Match } from "solid-js"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
|
|
||||||
@@ -41,6 +45,14 @@ type State = {
|
|||||||
todo: {
|
todo: {
|
||||||
[sessionID: string]: Todo[]
|
[sessionID: string]: Todo[]
|
||||||
}
|
}
|
||||||
|
permission: {
|
||||||
|
[sessionID: string]: Permission[]
|
||||||
|
}
|
||||||
|
mcp: {
|
||||||
|
[name: string]: McpStatus
|
||||||
|
}
|
||||||
|
lsp: LspStatus[]
|
||||||
|
vcs: VcsInfo | undefined
|
||||||
limit: number
|
limit: number
|
||||||
message: {
|
message: {
|
||||||
[sessionID: string]: Message[]
|
[sessionID: string]: Message[]
|
||||||
@@ -59,21 +71,19 @@ function createGlobalSync() {
|
|||||||
project: Project[]
|
project: Project[]
|
||||||
provider: ProviderListResponse
|
provider: ProviderListResponse
|
||||||
provider_auth: ProviderAuthResponse
|
provider_auth: ProviderAuthResponse
|
||||||
children: Record<string, State>
|
|
||||||
}>({
|
}>({
|
||||||
ready: false,
|
ready: false,
|
||||||
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||||
project: [],
|
project: [],
|
||||||
provider: { all: [], connected: [], default: {} },
|
provider: { all: [], connected: [], default: {} },
|
||||||
provider_auth: {},
|
provider_auth: {},
|
||||||
children: {},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const children: Record<string, ReturnType<typeof createStore<State>>> = {}
|
const children: Record<string, ReturnType<typeof createStore<State>>> = {}
|
||||||
function child(directory: string) {
|
function child(directory: string) {
|
||||||
if (!directory) console.error("No directory provided")
|
if (!directory) console.error("No directory provided")
|
||||||
if (!children[directory]) {
|
if (!children[directory]) {
|
||||||
setGlobalStore("children", directory, {
|
children[directory] = createStore<State>({
|
||||||
project: "",
|
project: "",
|
||||||
provider: { all: [], connected: [], default: {} },
|
provider: { all: [], connected: [], default: {} },
|
||||||
config: {},
|
config: {},
|
||||||
@@ -85,11 +95,14 @@ function createGlobalSync() {
|
|||||||
session_status: {},
|
session_status: {},
|
||||||
session_diff: {},
|
session_diff: {},
|
||||||
todo: {},
|
todo: {},
|
||||||
|
permission: {},
|
||||||
|
mcp: {},
|
||||||
|
lsp: [],
|
||||||
|
vcs: undefined,
|
||||||
limit: 5,
|
limit: 5,
|
||||||
message: {},
|
message: {},
|
||||||
part: {},
|
part: {},
|
||||||
})
|
})
|
||||||
children[directory] = createStore(globalStore.children[directory])
|
|
||||||
bootstrapInstance(directory)
|
bootstrapInstance(directory)
|
||||||
}
|
}
|
||||||
return children[directory]
|
return children[directory]
|
||||||
@@ -111,7 +124,7 @@ function createGlobalSync() {
|
|||||||
const updated = new Date(s.time.updated).getTime()
|
const updated = new Date(s.time.updated).getTime()
|
||||||
return updated > fourHoursAgo
|
return updated > fourHoursAgo
|
||||||
})
|
})
|
||||||
setStore("session", sessions)
|
setStore("session", reconcile(sessions, { key: "id" }))
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error("Failed to load sessions", err)
|
console.error("Failed to load sessions", err)
|
||||||
@@ -122,7 +135,7 @@ function createGlobalSync() {
|
|||||||
|
|
||||||
async function bootstrapInstance(directory: string) {
|
async function bootstrapInstance(directory: string) {
|
||||||
if (!directory) return
|
if (!directory) return
|
||||||
const [, setStore] = child(directory)
|
const [store, setStore] = child(directory)
|
||||||
const sdk = createOpencodeClient({
|
const sdk = createOpencodeClient({
|
||||||
baseUrl: globalSDK.url,
|
baseUrl: globalSDK.url,
|
||||||
directory,
|
directory,
|
||||||
@@ -149,6 +162,38 @@ function createGlobalSync() {
|
|||||||
session: () => loadSessions(directory),
|
session: () => loadSessions(directory),
|
||||||
status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)),
|
status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)),
|
||||||
config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
|
config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
|
||||||
|
mcp: () => sdk.mcp.status().then((x) => setStore("mcp", x.data ?? {})),
|
||||||
|
lsp: () => sdk.lsp.status().then((x) => setStore("lsp", x.data ?? [])),
|
||||||
|
vcs: () => sdk.vcs.get().then((x) => setStore("vcs", x.data)),
|
||||||
|
permission: () =>
|
||||||
|
sdk.permission.list().then((x) => {
|
||||||
|
const grouped: Record<string, Permission[]> = {}
|
||||||
|
for (const perm of x.data ?? []) {
|
||||||
|
const existing = grouped[perm.sessionID]
|
||||||
|
if (existing) {
|
||||||
|
existing.push(perm)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
grouped[perm.sessionID] = [perm]
|
||||||
|
}
|
||||||
|
|
||||||
|
batch(() => {
|
||||||
|
for (const sessionID of Object.keys(store.permission)) {
|
||||||
|
if (grouped[sessionID]) continue
|
||||||
|
setStore("permission", sessionID, [])
|
||||||
|
}
|
||||||
|
for (const [sessionID, permissions] of Object.entries(grouped)) {
|
||||||
|
setStore(
|
||||||
|
"permission",
|
||||||
|
sessionID,
|
||||||
|
reconcile(
|
||||||
|
permissions.slice().sort((a, b) => a.id.localeCompare(b.id)),
|
||||||
|
{ key: "id" },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
await Promise.all(Object.values(load).map((p) => retry(p).catch((e) => setGlobalStore("error", e))))
|
await Promise.all(Object.values(load).map((p) => retry(p).catch((e) => setGlobalStore("error", e))))
|
||||||
.then(() => setStore("ready", true))
|
.then(() => setStore("ready", true))
|
||||||
@@ -215,13 +260,13 @@ function createGlobalSync() {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "session.diff":
|
case "session.diff":
|
||||||
setStore("session_diff", event.properties.sessionID, event.properties.diff)
|
setStore("session_diff", event.properties.sessionID, reconcile(event.properties.diff, { key: "file" }))
|
||||||
break
|
break
|
||||||
case "todo.updated":
|
case "todo.updated":
|
||||||
setStore("todo", event.properties.sessionID, event.properties.todos)
|
setStore("todo", event.properties.sessionID, reconcile(event.properties.todos, { key: "id" }))
|
||||||
break
|
break
|
||||||
case "session.status": {
|
case "session.status": {
|
||||||
setStore("session_status", event.properties.sessionID, event.properties.status)
|
setStore("session_status", event.properties.sessionID, reconcile(event.properties.status))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "message.updated": {
|
case "message.updated": {
|
||||||
@@ -295,6 +340,56 @@ function createGlobalSync() {
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case "vcs.branch.updated": {
|
||||||
|
setStore("vcs", { branch: event.properties.branch })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "permission.updated": {
|
||||||
|
const sessionID = event.properties.sessionID
|
||||||
|
const permissions = store.permission[sessionID]
|
||||||
|
if (!permissions) {
|
||||||
|
setStore("permission", sessionID, [event.properties])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = Binary.search(permissions, event.properties.id, (p) => p.id)
|
||||||
|
if (result.found) {
|
||||||
|
setStore("permission", sessionID, result.index, reconcile(event.properties))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
setStore(
|
||||||
|
"permission",
|
||||||
|
sessionID,
|
||||||
|
produce((draft) => {
|
||||||
|
draft.splice(result.index, 0, event.properties)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "permission.replied": {
|
||||||
|
const permissions = store.permission[event.properties.sessionID]
|
||||||
|
if (!permissions) break
|
||||||
|
const result = Binary.search(permissions, event.properties.permissionID, (p) => p.id)
|
||||||
|
if (!result.found) break
|
||||||
|
setStore(
|
||||||
|
"permission",
|
||||||
|
event.properties.sessionID,
|
||||||
|
produce((draft) => {
|
||||||
|
draft.splice(result.index, 1)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "lsp.updated": {
|
||||||
|
const sdk = createOpencodeClient({
|
||||||
|
baseUrl: globalSDK.url,
|
||||||
|
directory,
|
||||||
|
throwOnError: true,
|
||||||
|
})
|
||||||
|
sdk.lsp.status().then((x) => setStore("lsp", x.data ?? []))
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
{
|
{
|
||||||
...project,
|
...project,
|
||||||
...(metadata ?? {}),
|
...(metadata ?? {}),
|
||||||
|
icon: { url: metadata?.icon?.url, color: metadata?.icon?.color },
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
import { createStore, produce, reconcile } from "solid-js/store"
|
||||||
import { batch, createEffect, createMemo } from "solid-js"
|
import { batch, createMemo } from "solid-js"
|
||||||
import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda"
|
import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda"
|
||||||
import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2"
|
import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
@@ -62,24 +62,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({
|
|
||||||
// type: "warning",
|
|
||||||
// message: `Agent ${value.name}'s configured model ${value.model.providerID}/${value.model.modelID} is not valid`,
|
|
||||||
// duration: 3000,
|
|
||||||
// })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
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<{
|
||||||
@@ -395,17 +377,20 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const list = async (path: string) => {
|
const list = async (path: string) => {
|
||||||
return sdk.client.file.list({ path: path + "/" }).then((x) => {
|
return sdk.client.file
|
||||||
setStore(
|
.list({ path: path + "/" })
|
||||||
"node",
|
.then((x) => {
|
||||||
produce((draft) => {
|
setStore(
|
||||||
x.data!.forEach((node) => {
|
"node",
|
||||||
if (node.path in draft) return
|
produce((draft) => {
|
||||||
draft[node.path] = node
|
x.data!.forEach((node) => {
|
||||||
})
|
if (node.path in draft) return
|
||||||
}),
|
draft[node.path] = node
|
||||||
)
|
})
|
||||||
})
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchFiles = (query: string) => sdk.client.find.files({ query, dirs: "false" }).then((x) => x.data!)
|
const searchFiles = (query: string) => sdk.client.find.files({ query, dirs: "false" }).then((x) => x.data!)
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import { createEffect, createRoot, onCleanup } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
|
import type { Permission } from "@opencode-ai/sdk/v2/client"
|
||||||
|
import { persisted } from "@/utils/persist"
|
||||||
|
|
||||||
|
type PermissionsBySession = {
|
||||||
|
[sessionID: string]: Permission[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type PermissionRespondFn = (input: {
|
||||||
|
sessionID: string
|
||||||
|
permissionID: string
|
||||||
|
response: "once" | "always" | "reject"
|
||||||
|
}) => void
|
||||||
|
|
||||||
|
const AUTO_ACCEPT_TYPES = new Set(["edit", "write"])
|
||||||
|
|
||||||
|
function shouldAutoAccept(perm: Permission) {
|
||||||
|
return AUTO_ACCEPT_TYPES.has(perm.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const { use: usePermission, provider: PermissionProvider } = createSimpleContext({
|
||||||
|
name: "Permission",
|
||||||
|
init: (props: { permissions: PermissionsBySession; onRespond: PermissionRespondFn }) => {
|
||||||
|
const [store, setStore, _, ready] = persisted(
|
||||||
|
"permission.v1",
|
||||||
|
createStore({
|
||||||
|
autoAcceptEdits: {} as Record<string, boolean>,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const responded = new Set<string>()
|
||||||
|
const watches = new Map<string, () => void>()
|
||||||
|
|
||||||
|
function respond(perm: Permission) {
|
||||||
|
if (responded.has(perm.id)) return
|
||||||
|
responded.add(perm.id)
|
||||||
|
props.onRespond({
|
||||||
|
sessionID: perm.sessionID,
|
||||||
|
permissionID: perm.id,
|
||||||
|
response: "once",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function watch(sessionID: string) {
|
||||||
|
if (watches.has(sessionID)) return
|
||||||
|
|
||||||
|
const dispose = createRoot((dispose) => {
|
||||||
|
createEffect(() => {
|
||||||
|
if (!store.autoAcceptEdits[sessionID]) return
|
||||||
|
|
||||||
|
const permissions = props.permissions[sessionID] ?? []
|
||||||
|
permissions.length
|
||||||
|
|
||||||
|
for (const perm of permissions) {
|
||||||
|
if (!shouldAutoAccept(perm)) continue
|
||||||
|
respond(perm)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return dispose
|
||||||
|
})
|
||||||
|
|
||||||
|
watches.set(sessionID, dispose)
|
||||||
|
}
|
||||||
|
|
||||||
|
function unwatch(sessionID: string) {
|
||||||
|
const dispose = watches.get(sessionID)
|
||||||
|
if (!dispose) return
|
||||||
|
dispose()
|
||||||
|
watches.delete(sessionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!ready()) return
|
||||||
|
|
||||||
|
for (const sessionID in store.autoAcceptEdits) {
|
||||||
|
if (!store.autoAcceptEdits[sessionID]) continue
|
||||||
|
watch(sessionID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
for (const dispose of watches.values()) dispose()
|
||||||
|
watches.clear()
|
||||||
|
})
|
||||||
|
|
||||||
|
function enable(sessionID: string) {
|
||||||
|
setStore("autoAcceptEdits", sessionID, true)
|
||||||
|
watch(sessionID)
|
||||||
|
|
||||||
|
const permissions = props.permissions[sessionID] ?? []
|
||||||
|
for (const perm of permissions) {
|
||||||
|
if (!shouldAutoAccept(perm)) continue
|
||||||
|
respond(perm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function disable(sessionID: string) {
|
||||||
|
setStore("autoAcceptEdits", sessionID, false)
|
||||||
|
unwatch(sessionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
get permissions() {
|
||||||
|
return props.permissions
|
||||||
|
},
|
||||||
|
respond: props.onRespond,
|
||||||
|
isAutoAccepting(sessionID: string) {
|
||||||
|
return store.autoAcceptEdits[sessionID] ?? false
|
||||||
|
},
|
||||||
|
toggleAutoAccept(sessionID: string) {
|
||||||
|
if (store.autoAcceptEdits[sessionID]) {
|
||||||
|
disable(sessionID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
enable(sessionID)
|
||||||
|
},
|
||||||
|
enableAutoAccept(sessionID: string) {
|
||||||
|
if (store.autoAcceptEdits[sessionID]) return
|
||||||
|
enable(sessionID)
|
||||||
|
},
|
||||||
|
disableAutoAccept(sessionID: string) {
|
||||||
|
disable(sessionID)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { produce } from "solid-js/store"
|
import { batch, createMemo } from "solid-js"
|
||||||
import { createMemo } from "solid-js"
|
import { produce, reconcile } from "solid-js/store"
|
||||||
import { Binary } from "@opencode-ai/util/binary"
|
import { Binary } from "@opencode-ai/util/binary"
|
||||||
import { retry } from "@opencode-ai/util/retry"
|
import { retry } from "@opencode-ai/util/retry"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
@@ -56,7 +56,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
const result = Binary.search(messages, input.messageID, (m) => m.id)
|
const result = Binary.search(messages, input.messageID, (m) => m.id)
|
||||||
messages.splice(result.index, 0, message)
|
messages.splice(result.index, 0, message)
|
||||||
}
|
}
|
||||||
draft.part[input.messageID] = input.parts.slice()
|
draft.part[input.messageID] = input.parts.slice().sort((a, b) => a.id.localeCompare(b.id))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -67,22 +67,46 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
retry(() => sdk.client.session.todo({ sessionID })),
|
retry(() => sdk.client.session.todo({ sessionID })),
|
||||||
retry(() => sdk.client.session.diff({ sessionID })),
|
retry(() => sdk.client.session.diff({ sessionID })),
|
||||||
])
|
])
|
||||||
setStore(
|
|
||||||
produce((draft) => {
|
batch(() => {
|
||||||
const match = Binary.search(draft.session, sessionID, (s) => s.id)
|
setStore(
|
||||||
if (match.found) draft.session[match.index] = session.data!
|
"session",
|
||||||
if (!match.found) draft.session.splice(match.index, 0, session.data!)
|
produce((draft) => {
|
||||||
draft.todo[sessionID] = todo.data ?? []
|
const match = Binary.search(draft, sessionID, (s) => s.id)
|
||||||
draft.message[sessionID] = messages
|
if (match.found) {
|
||||||
.data!.map((x) => x.info)
|
draft[match.index] = session.data!
|
||||||
.slice()
|
return
|
||||||
.sort((a, b) => a.id.localeCompare(b.id))
|
}
|
||||||
for (const message of messages.data!) {
|
draft.splice(match.index, 0, session.data!)
|
||||||
draft.part[message.info.id] = message.parts.slice().sort((a, b) => a.id.localeCompare(b.id))
|
}),
|
||||||
}
|
)
|
||||||
draft.session_diff[sessionID] = diff.data ?? []
|
|
||||||
}),
|
setStore("todo", sessionID, reconcile(todo.data ?? [], { key: "id" }))
|
||||||
)
|
setStore(
|
||||||
|
"message",
|
||||||
|
sessionID,
|
||||||
|
reconcile(
|
||||||
|
(messages.data ?? [])
|
||||||
|
.map((x) => x.info)
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => a.id.localeCompare(b.id)),
|
||||||
|
{ key: "id" },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const message of messages.data ?? []) {
|
||||||
|
setStore(
|
||||||
|
"part",
|
||||||
|
message.info.id,
|
||||||
|
reconcile(
|
||||||
|
message.parts.slice().sort((a, b) => a.id.localeCompare(b.id)),
|
||||||
|
{ key: "id" },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setStore("session_diff", sessionID, reconcile(diff.data ?? [], { key: "file" }))
|
||||||
|
})
|
||||||
},
|
},
|
||||||
fetch: async (count = 10) => {
|
fetch: async (count = 10) => {
|
||||||
setStore("limit", (x) => x + count)
|
setStore("limit", (x) => x + count)
|
||||||
@@ -91,7 +115,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => a.id.localeCompare(b.id))
|
.sort((a, b) => a.id.localeCompare(b.id))
|
||||||
.slice(0, store.limit)
|
.slice(0, store.limit)
|
||||||
setStore("session", sessions)
|
setStore("session", reconcile(sessions, { key: "id" }))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
more: createMemo(() => store.session.length >= store.limit),
|
more: createMemo(() => store.session.length >= store.limit),
|
||||||
|
|||||||
@@ -36,35 +36,49 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
|
|||||||
all: createMemo(() => Object.values(store.all)),
|
all: createMemo(() => Object.values(store.all)),
|
||||||
active: createMemo(() => store.active),
|
active: createMemo(() => store.active),
|
||||||
new() {
|
new() {
|
||||||
sdk.client.pty.create({ title: `Terminal ${store.all.length + 1}` }).then((pty) => {
|
sdk.client.pty
|
||||||
const id = pty.data?.id
|
.create({ title: `Terminal ${store.all.length + 1}` })
|
||||||
if (!id) return
|
.then((pty) => {
|
||||||
setStore("all", [
|
const id = pty.data?.id
|
||||||
...store.all,
|
if (!id) return
|
||||||
{
|
setStore("all", [
|
||||||
id,
|
...store.all,
|
||||||
title: pty.data?.title ?? "Terminal",
|
{
|
||||||
},
|
id,
|
||||||
])
|
title: pty.data?.title ?? "Terminal",
|
||||||
setStore("active", id)
|
},
|
||||||
})
|
])
|
||||||
|
setStore("active", id)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("Failed to create terminal", e)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
update(pty: Partial<LocalPTY> & { id: string }) {
|
update(pty: Partial<LocalPTY> & { id: string }) {
|
||||||
setStore("all", (x) => x.map((x) => (x.id === pty.id ? { ...x, ...pty } : x)))
|
setStore("all", (x) => x.map((x) => (x.id === pty.id ? { ...x, ...pty } : x)))
|
||||||
sdk.client.pty.update({
|
sdk.client.pty
|
||||||
ptyID: pty.id,
|
.update({
|
||||||
title: pty.title,
|
ptyID: pty.id,
|
||||||
size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
|
title: pty.title,
|
||||||
})
|
size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("Failed to update terminal", e)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
async clone(id: string) {
|
async clone(id: string) {
|
||||||
const index = store.all.findIndex((x) => x.id === id)
|
const index = store.all.findIndex((x) => x.id === id)
|
||||||
const pty = store.all[index]
|
const pty = store.all[index]
|
||||||
if (!pty) return
|
if (!pty) return
|
||||||
const clone = await sdk.client.pty.create({
|
const clone = await sdk.client.pty
|
||||||
title: pty.title,
|
.create({
|
||||||
})
|
title: pty.title,
|
||||||
if (!clone.data) return
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("Failed to clone terminal", e)
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
if (!clone?.data) return
|
||||||
setStore("all", index, {
|
setStore("all", index, {
|
||||||
...pty,
|
...pty,
|
||||||
...clone.data,
|
...clone.data,
|
||||||
@@ -88,7 +102,9 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
|
|||||||
setStore("active", previous?.id)
|
setStore("active", previous?.id)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await sdk.client.pty.remove({ ptyID: id })
|
await sdk.client.pty.remove({ ptyID: id }).catch((e) => {
|
||||||
|
console.error("Failed to close terminal", e)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
move(id: string, to: number) {
|
move(id: string, to: number) {
|
||||||
const index = store.all.findIndex((f) => f.id === id)
|
const index = store.all.findIndex((f) => f.id === id)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { createMemo, Show, type ParentProps } from "solid-js"
|
import { createMemo, Show, type ParentProps } from "solid-js"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { SDKProvider } from "@/context/sdk"
|
import { SDKProvider, useSDK } from "@/context/sdk"
|
||||||
import { SyncProvider, useSync } from "@/context/sync"
|
import { SyncProvider, useSync } from "@/context/sync"
|
||||||
import { LocalProvider } from "@/context/local"
|
import { LocalProvider } from "@/context/local"
|
||||||
|
import { PermissionProvider } from "@/context/permission"
|
||||||
import { base64Decode } from "@opencode-ai/util/encode"
|
import { base64Decode } from "@opencode-ai/util/encode"
|
||||||
import { DataProvider } from "@opencode-ai/ui/context"
|
import { DataProvider } from "@opencode-ai/ui/context"
|
||||||
import { iife } from "@opencode-ai/util/iife"
|
import { iife } from "@opencode-ai/util/iife"
|
||||||
@@ -18,10 +19,19 @@ export default function Layout(props: ParentProps) {
|
|||||||
<SyncProvider>
|
<SyncProvider>
|
||||||
{iife(() => {
|
{iife(() => {
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
|
const sdk = useSDK()
|
||||||
|
const respond = (input: {
|
||||||
|
sessionID: string
|
||||||
|
permissionID: string
|
||||||
|
response: "once" | "always" | "reject"
|
||||||
|
}) => sdk.client.permission.respond(input)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataProvider data={sync.data} directory={directory()}>
|
<PermissionProvider permissions={sync.data.permission} onRespond={respond}>
|
||||||
<LocalProvider>{props.children}</LocalProvider>
|
<DataProvider data={sync.data} directory={directory()} onPermissionRespond={respond}>
|
||||||
</DataProvider>
|
<LocalProvider>{props.children}</LocalProvider>
|
||||||
|
</DataProvider>
|
||||||
|
</PermissionProvider>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</SyncProvider>
|
</SyncProvider>
|
||||||
|
|||||||
@@ -20,11 +20,51 @@ function isInitError(error: unknown): error is InitError {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function safeJson(value: unknown): string {
|
||||||
|
const seen = new WeakSet<object>()
|
||||||
|
const json = JSON.stringify(
|
||||||
|
value,
|
||||||
|
(_key, val) => {
|
||||||
|
if (typeof val === "bigint") return val.toString()
|
||||||
|
if (typeof val === "object" && val) {
|
||||||
|
if (seen.has(val)) return "[Circular]"
|
||||||
|
seen.add(val)
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
},
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
return json ?? String(value)
|
||||||
|
}
|
||||||
|
|
||||||
function formatInitError(error: InitError): string {
|
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
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
ParentProps,
|
ParentProps,
|
||||||
Show,
|
Show,
|
||||||
Switch,
|
Switch,
|
||||||
|
untrack,
|
||||||
type JSX,
|
type JSX,
|
||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
import { DateTime } from "luxon"
|
import { DateTime } from "luxon"
|
||||||
@@ -40,14 +41,16 @@ import {
|
|||||||
} from "@thisbeyond/solid-dnd"
|
} from "@thisbeyond/solid-dnd"
|
||||||
import type { DragEvent } from "@thisbeyond/solid-dnd"
|
import type { DragEvent } from "@thisbeyond/solid-dnd"
|
||||||
import { useProviders } from "@/hooks/use-providers"
|
import { useProviders } from "@/hooks/use-providers"
|
||||||
import { showToast, Toast } from "@opencode-ai/ui/toast"
|
import { showToast, Toast, toaster } from "@opencode-ai/ui/toast"
|
||||||
import { useGlobalSDK } from "@/context/global-sdk"
|
import { useGlobalSDK } from "@/context/global-sdk"
|
||||||
import { useNotification } from "@/context/notification"
|
import { useNotification } from "@/context/notification"
|
||||||
import { Binary } from "@opencode-ai/util/binary"
|
import { Binary } from "@opencode-ai/util/binary"
|
||||||
import { Header } from "@/components/header"
|
import { Header } from "@/components/header"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
|
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
||||||
import { DialogSelectProvider } from "@/components/dialog-select-provider"
|
import { DialogSelectProvider } from "@/components/dialog-select-provider"
|
||||||
import { useCommand } from "@/context/command"
|
import { DialogEditProject } from "@/components/dialog-edit-project"
|
||||||
|
import { useCommand, type CommandOption } from "@/context/command"
|
||||||
import { ConstrainDragXAxis } from "@/utils/solid-dnd"
|
import { ConstrainDragXAxis } from "@/utils/solid-dnd"
|
||||||
|
|
||||||
export default function Layout(props: ParentProps) {
|
export default function Layout(props: ParentProps) {
|
||||||
@@ -88,6 +91,41 @@ export default function Layout(props: ParentProps) {
|
|||||||
const providers = useProviders()
|
const providers = useProviders()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
|
const theme = useTheme()
|
||||||
|
const availableThemeEntries = createMemo(() => Object.entries(theme.themes()))
|
||||||
|
const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"]
|
||||||
|
const colorSchemeLabel: Record<ColorScheme, string> = {
|
||||||
|
system: "System",
|
||||||
|
light: "Light",
|
||||||
|
dark: "Dark",
|
||||||
|
}
|
||||||
|
|
||||||
|
function cycleTheme(direction = 1) {
|
||||||
|
const ids = availableThemeEntries().map(([id]) => id)
|
||||||
|
if (ids.length === 0) return
|
||||||
|
const currentIndex = ids.indexOf(theme.themeId())
|
||||||
|
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + direction + ids.length) % ids.length
|
||||||
|
const nextThemeId = ids[nextIndex]
|
||||||
|
theme.setTheme(nextThemeId)
|
||||||
|
const nextTheme = theme.themes()[nextThemeId]
|
||||||
|
showToast({
|
||||||
|
title: "Theme switched",
|
||||||
|
description: nextTheme?.name ?? nextThemeId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function cycleColorScheme(direction = 1) {
|
||||||
|
const current = theme.colorScheme()
|
||||||
|
const currentIndex = colorSchemeOrder.indexOf(current)
|
||||||
|
const nextIndex =
|
||||||
|
currentIndex === -1 ? 0 : (currentIndex + direction + colorSchemeOrder.length) % colorSchemeOrder.length
|
||||||
|
const next = colorSchemeOrder[nextIndex]
|
||||||
|
theme.setColorScheme(next)
|
||||||
|
showToast({
|
||||||
|
title: "Color scheme",
|
||||||
|
description: colorSchemeLabel[next],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (platform.checkUpdate && platform.update && platform.restart) {
|
if (platform.checkUpdate && platform.update && platform.restart) {
|
||||||
@@ -116,6 +154,71 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const seenSessions = new Set<string>()
|
||||||
|
const toastBySession = new Map<string, number>()
|
||||||
|
const unsub = globalSDK.event.listen((e) => {
|
||||||
|
if (e.details?.type !== "permission.updated") return
|
||||||
|
const directory = e.name
|
||||||
|
const permission = e.details.properties
|
||||||
|
const sessionKey = `${directory}:${permission.sessionID}`
|
||||||
|
if (seenSessions.has(sessionKey)) return
|
||||||
|
seenSessions.add(sessionKey)
|
||||||
|
const currentDir = params.dir ? base64Decode(params.dir) : undefined
|
||||||
|
const currentSession = params.id
|
||||||
|
if (directory === currentDir && permission.sessionID === currentSession) return
|
||||||
|
const [store] = globalSync.child(directory)
|
||||||
|
const session = store.session.find((s) => s.id === permission.sessionID)
|
||||||
|
if (directory === currentDir && session?.parentID === currentSession) return
|
||||||
|
const sessionTitle = session?.title ?? "New session"
|
||||||
|
const projectName = getFilename(directory)
|
||||||
|
const toastId = showToast({
|
||||||
|
persistent: true,
|
||||||
|
icon: "checklist",
|
||||||
|
title: "Permission required",
|
||||||
|
description: `${sessionTitle} in ${projectName} needs permission`,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: "Go to session",
|
||||||
|
onClick: () => {
|
||||||
|
navigate(`/${base64Encode(directory)}/session/${permission.sessionID}`)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Dismiss",
|
||||||
|
onClick: "dismiss",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
toastBySession.set(sessionKey, toastId)
|
||||||
|
})
|
||||||
|
onCleanup(unsub)
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const currentDir = params.dir ? base64Decode(params.dir) : undefined
|
||||||
|
const currentSession = params.id
|
||||||
|
if (!currentDir || !currentSession) return
|
||||||
|
const sessionKey = `${currentDir}:${currentSession}`
|
||||||
|
const toastId = toastBySession.get(sessionKey)
|
||||||
|
if (toastId !== undefined) {
|
||||||
|
toaster.dismiss(toastId)
|
||||||
|
toastBySession.delete(sessionKey)
|
||||||
|
seenSessions.delete(sessionKey)
|
||||||
|
}
|
||||||
|
const [store] = globalSync.child(currentDir)
|
||||||
|
const childSessions = store.session.filter((s) => s.parentID === currentSession)
|
||||||
|
for (const child of childSessions) {
|
||||||
|
const childKey = `${currentDir}:${child.id}`
|
||||||
|
const childToastId = toastBySession.get(childKey)
|
||||||
|
if (childToastId !== undefined) {
|
||||||
|
toaster.dismiss(childToastId)
|
||||||
|
toastBySession.delete(childKey)
|
||||||
|
seenSessions.delete(childKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
function sortSessions(a: Session, b: Session) {
|
function sortSessions(a: Session, b: Session) {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const oneMinuteAgo = now - 60 * 1000
|
const oneMinuteAgo = now - 60 * 1000
|
||||||
@@ -220,57 +323,102 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
command.register(() => [
|
command.register(() => {
|
||||||
{
|
const commands: CommandOption[] = [
|
||||||
id: "sidebar.toggle",
|
{
|
||||||
title: "Toggle sidebar",
|
id: "sidebar.toggle",
|
||||||
category: "View",
|
title: "Toggle sidebar",
|
||||||
keybind: "mod+b",
|
category: "View",
|
||||||
onSelect: () => layout.sidebar.toggle(),
|
keybind: "mod+b",
|
||||||
},
|
onSelect: () => layout.sidebar.toggle(),
|
||||||
...(platform.openDirectoryPickerDialog
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
id: "project.open",
|
|
||||||
title: "Open project",
|
|
||||||
category: "Project",
|
|
||||||
keybind: "mod+o",
|
|
||||||
onSelect: () => chooseProject(),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
|
||||||
id: "provider.connect",
|
|
||||||
title: "Connect provider",
|
|
||||||
category: "Provider",
|
|
||||||
onSelect: () => connectProvider(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "session.previous",
|
|
||||||
title: "Previous session",
|
|
||||||
category: "Session",
|
|
||||||
keybind: "alt+arrowup",
|
|
||||||
onSelect: () => navigateSessionByOffset(-1),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "session.next",
|
|
||||||
title: "Next session",
|
|
||||||
category: "Session",
|
|
||||||
keybind: "alt+arrowdown",
|
|
||||||
onSelect: () => navigateSessionByOffset(1),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "session.archive",
|
|
||||||
title: "Archive session",
|
|
||||||
category: "Session",
|
|
||||||
keybind: "mod+shift+backspace",
|
|
||||||
disabled: !params.dir || !params.id,
|
|
||||||
onSelect: () => {
|
|
||||||
const session = currentSessions().find((s) => s.id === params.id)
|
|
||||||
if (session) archiveSession(session)
|
|
||||||
},
|
},
|
||||||
},
|
...(platform.openDirectoryPickerDialog
|
||||||
])
|
? [
|
||||||
|
{
|
||||||
|
id: "project.open",
|
||||||
|
title: "Open project",
|
||||||
|
category: "Project",
|
||||||
|
keybind: "mod+o",
|
||||||
|
onSelect: () => chooseProject(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
id: "provider.connect",
|
||||||
|
title: "Connect provider",
|
||||||
|
category: "Provider",
|
||||||
|
onSelect: () => connectProvider(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "session.previous",
|
||||||
|
title: "Previous session",
|
||||||
|
category: "Session",
|
||||||
|
keybind: "alt+arrowup",
|
||||||
|
onSelect: () => navigateSessionByOffset(-1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "session.next",
|
||||||
|
title: "Next session",
|
||||||
|
category: "Session",
|
||||||
|
keybind: "alt+arrowdown",
|
||||||
|
onSelect: () => navigateSessionByOffset(1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "session.archive",
|
||||||
|
title: "Archive session",
|
||||||
|
category: "Session",
|
||||||
|
keybind: "mod+shift+backspace",
|
||||||
|
disabled: !params.dir || !params.id,
|
||||||
|
onSelect: () => {
|
||||||
|
const session = currentSessions().find((s) => s.id === params.id)
|
||||||
|
if (session) archiveSession(session)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "theme.cycle",
|
||||||
|
title: "Cycle theme",
|
||||||
|
category: "Theme",
|
||||||
|
keybind: "mod+shift+t",
|
||||||
|
onSelect: () => cycleTheme(1),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const [id, definition] of availableThemeEntries()) {
|
||||||
|
commands.push({
|
||||||
|
id: `theme.set.${id}`,
|
||||||
|
title: `Use theme: ${definition.name ?? id}`,
|
||||||
|
category: "Theme",
|
||||||
|
onSelect: () => theme.commitPreview(),
|
||||||
|
onHighlight: () => {
|
||||||
|
theme.previewTheme(id)
|
||||||
|
return () => theme.cancelPreview()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
commands.push({
|
||||||
|
id: "theme.scheme.cycle",
|
||||||
|
title: "Cycle color scheme",
|
||||||
|
category: "Theme",
|
||||||
|
keybind: "mod+shift+s",
|
||||||
|
onSelect: () => cycleColorScheme(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const scheme of colorSchemeOrder) {
|
||||||
|
commands.push({
|
||||||
|
id: `theme.scheme.${scheme}`,
|
||||||
|
title: `Use color scheme: ${colorSchemeLabel[scheme]}`,
|
||||||
|
category: "Theme",
|
||||||
|
onSelect: () => theme.commitPreview(),
|
||||||
|
onHighlight: () => {
|
||||||
|
theme.previewColorScheme(scheme)
|
||||||
|
return () => theme.cancelPreview()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return commands
|
||||||
|
})
|
||||||
|
|
||||||
function connectProvider() {
|
function connectProvider() {
|
||||||
dialog.show(() => <DialogSelectProvider />)
|
dialog.show(() => <DialogSelectProvider />)
|
||||||
@@ -323,7 +471,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
const id = params.id
|
const id = params.id
|
||||||
setStore("lastSession", directory, id)
|
setStore("lastSession", directory, id)
|
||||||
notification.session.markViewed(id)
|
notification.session.markViewed(id)
|
||||||
layout.projects.expand(directory)
|
untrack(() => layout.projects.expand(directory))
|
||||||
requestAnimationFrame(() => scrollToSession(id))
|
requestAnimationFrame(() => scrollToSession(id))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -375,7 +523,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
const notification = useNotification()
|
const notification = useNotification()
|
||||||
const notifications = createMemo(() => notification.project.unseen(props.project.worktree))
|
const notifications = createMemo(() => notification.project.unseen(props.project.worktree))
|
||||||
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
|
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
|
||||||
const name = createMemo(() => getFilename(props.project.worktree))
|
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
|
||||||
const mask = "radial-gradient(circle 5px at calc(100% - 2px) 2px, transparent 5px, black 5.5px)"
|
const mask = "radial-gradient(circle 5px at calc(100% - 2px) 2px, transparent 5px, black 5.5px)"
|
||||||
const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
|
const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
|
||||||
|
|
||||||
@@ -411,7 +559,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ProjectVisual = (props: { project: LocalProject; class?: string }): JSX.Element => {
|
const ProjectVisual = (props: { project: LocalProject; class?: string }): JSX.Element => {
|
||||||
const name = createMemo(() => getFilename(props.project.worktree))
|
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
|
||||||
const current = createMemo(() => base64Decode(params.dir ?? ""))
|
const current = createMemo(() => base64Decode(params.dir ?? ""))
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
@@ -453,8 +601,20 @@ export default function Layout(props: ParentProps) {
|
|||||||
const updated = createMemo(() => DateTime.fromMillis(props.session.time.updated))
|
const updated = createMemo(() => DateTime.fromMillis(props.session.time.updated))
|
||||||
const notifications = createMemo(() => notification.session.unseen(props.session.id))
|
const notifications = createMemo(() => notification.session.unseen(props.session.id))
|
||||||
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
|
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
|
||||||
|
const hasPermissions = createMemo(() => {
|
||||||
|
const store = globalSync.child(props.project.worktree)[0]
|
||||||
|
const permissions = store.permission?.[props.session.id] ?? []
|
||||||
|
if (permissions.length > 0) return true
|
||||||
|
const childSessions = store.session.filter((s) => s.parentID === props.session.id)
|
||||||
|
for (const child of childSessions) {
|
||||||
|
const childPermissions = store.permission?.[child.id] ?? []
|
||||||
|
if (childPermissions.length > 0) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
const isWorking = createMemo(() => {
|
const isWorking = createMemo(() => {
|
||||||
if (props.session.id === params.id) return false
|
if (props.session.id === params.id) return false
|
||||||
|
if (hasPermissions()) return false
|
||||||
const status = globalSync.child(props.project.worktree)[0].session_status[props.session.id]
|
const status = globalSync.child(props.project.worktree)[0].session_status[props.session.id]
|
||||||
return status?.type === "busy" || status?.type === "retry"
|
return status?.type === "busy" || status?.type === "retry"
|
||||||
})
|
})
|
||||||
@@ -472,7 +632,12 @@ export default function Layout(props: ParentProps) {
|
|||||||
class="flex flex-col min-w-0 text-left w-full focus:outline-none"
|
class="flex flex-col min-w-0 text-left w-full focus:outline-none"
|
||||||
>
|
>
|
||||||
<div class="flex items-center self-stretch gap-6 justify-between transition-[padding] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7">
|
<div class="flex items-center self-stretch gap-6 justify-between transition-[padding] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7">
|
||||||
<span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
|
<span
|
||||||
|
classList={{
|
||||||
|
"text-14-regular text-text-strong overflow-hidden text-ellipsis truncate": true,
|
||||||
|
"animate-pulse": isWorking(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
{props.session.title}
|
{props.session.title}
|
||||||
</span>
|
</span>
|
||||||
<div class="shrink-0 group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
|
<div class="shrink-0 group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
|
||||||
@@ -480,6 +645,9 @@ export default function Layout(props: ParentProps) {
|
|||||||
<Match when={isWorking()}>
|
<Match when={isWorking()}>
|
||||||
<Spinner class="size-2.5 mr-0.5" />
|
<Spinner class="size-2.5 mr-0.5" />
|
||||||
</Match>
|
</Match>
|
||||||
|
<Match when={hasPermissions()}>
|
||||||
|
<div class="size-1.5 mr-1.5 rounded-full bg-surface-warning-strong" />
|
||||||
|
</Match>
|
||||||
<Match when={hasError()}>
|
<Match when={hasError()}>
|
||||||
<div class="size-1.5 mr-1.5 rounded-full bg-text-diff-delete-base" />
|
<div class="size-1.5 mr-1.5 rounded-full bg-text-diff-delete-base" />
|
||||||
</Match>
|
</Match>
|
||||||
@@ -513,7 +681,15 @@ export default function Layout(props: ParentProps) {
|
|||||||
</A>
|
</A>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div class="hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute top-1 right-1">
|
<div class="hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute top-1 right-1">
|
||||||
<Tooltip placement={props.mobile ? "bottom" : "right"} value="Archive session">
|
<Tooltip
|
||||||
|
placement={props.mobile ? "bottom" : "right"}
|
||||||
|
value={
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span>Archive session</span>
|
||||||
|
<span class="text-icon-base text-12-medium">{command.keybind("session.archive")}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<IconButton icon="archive" variant="ghost" onClick={() => archiveSession(props.session)} />
|
<IconButton icon="archive" variant="ghost" onClick={() => archiveSession(props.session)} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -526,7 +702,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
const sortable = createSortable(props.project.worktree)
|
const sortable = createSortable(props.project.worktree)
|
||||||
const showExpanded = createMemo(() => props.mobile || layout.sidebar.opened())
|
const showExpanded = createMemo(() => props.mobile || layout.sidebar.opened())
|
||||||
const slug = createMemo(() => base64Encode(props.project.worktree))
|
const slug = createMemo(() => base64Encode(props.project.worktree))
|
||||||
const name = createMemo(() => getFilename(props.project.worktree))
|
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
|
||||||
const [store, setProjectStore] = globalSync.child(props.project.worktree)
|
const [store, setProjectStore] = globalSync.child(props.project.worktree)
|
||||||
const sessions = createMemo(() => store.session.toSorted(sortSessions))
|
const sessions = createMemo(() => store.session.toSorted(sortSessions))
|
||||||
const rootSessions = createMemo(() => sessions().filter((s) => !s.parentID))
|
const rootSessions = createMemo(() => sessions().filter((s) => !s.parentID))
|
||||||
@@ -572,13 +748,26 @@ export default function Layout(props: ParentProps) {
|
|||||||
<DropdownMenu.Trigger as={IconButton} icon="dot-grid" variant="ghost" />
|
<DropdownMenu.Trigger as={IconButton} icon="dot-grid" variant="ghost" />
|
||||||
<DropdownMenu.Portal>
|
<DropdownMenu.Portal>
|
||||||
<DropdownMenu.Content>
|
<DropdownMenu.Content>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onSelect={() => dialog.show(() => <DialogEditProject project={props.project} />)}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemLabel>Edit project</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item onSelect={() => closeProject(props.project.worktree)}>
|
<DropdownMenu.Item onSelect={() => closeProject(props.project.worktree)}>
|
||||||
<DropdownMenu.ItemLabel>Close Project</DropdownMenu.ItemLabel>
|
<DropdownMenu.ItemLabel>Close project</DropdownMenu.ItemLabel>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Portal>
|
</DropdownMenu.Portal>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<Tooltip placement="top" value="New session">
|
<Tooltip
|
||||||
|
placement="top"
|
||||||
|
value={
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span>New session</span>
|
||||||
|
<span class="text-icon-base text-12-medium">{command.keybind("session.new")}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<IconButton as={A} href={`${slug()}/session`} icon="plus-small" variant="ghost" />
|
<IconButton as={A} href={`${slug()}/session`} icon="plus-small" variant="ghost" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -727,7 +916,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>
|
||||||
@@ -746,7 +935,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"
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import { checksum } from "@opencode-ai/util/encode"
|
|||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { DialogSelectFile } from "@/components/dialog-select-file"
|
import { DialogSelectFile } from "@/components/dialog-select-file"
|
||||||
import { DialogSelectModel } from "@/components/dialog-select-model"
|
import { DialogSelectModel } from "@/components/dialog-select-model"
|
||||||
|
import { DialogSelectMcp } from "@/components/dialog-select-mcp"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
import { useNavigate, useParams } from "@solidjs/router"
|
import { useNavigate, useParams } from "@solidjs/router"
|
||||||
import { UserMessage } from "@opencode-ai/sdk/v2"
|
import { UserMessage } from "@opencode-ai/sdk/v2"
|
||||||
@@ -56,6 +57,17 @@ import { useSDK } from "@/context/sdk"
|
|||||||
import { usePrompt } from "@/context/prompt"
|
import { usePrompt } from "@/context/prompt"
|
||||||
import { extractPromptFromParts } from "@/utils/prompt"
|
import { extractPromptFromParts } from "@/utils/prompt"
|
||||||
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
|
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
|
||||||
|
import { StatusBar } from "@/components/status-bar"
|
||||||
|
import { SessionMcpIndicator } from "@/components/session-mcp-indicator"
|
||||||
|
import { SessionLspIndicator } from "@/components/session-lsp-indicator"
|
||||||
|
import { usePermission } from "@/context/permission"
|
||||||
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
|
|
||||||
|
function same<T>(a: readonly T[], b: readonly T[]) {
|
||||||
|
if (a === b) return true
|
||||||
|
if (a.length !== b.length) return false
|
||||||
|
return a.every((x, i) => x === b[i])
|
||||||
|
}
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
@@ -70,22 +82,40 @@ export default function Page() {
|
|||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
|
|
||||||
|
const permission = usePermission()
|
||||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||||
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
||||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||||
const revertMessageID = createMemo(() => info()?.revert?.messageID)
|
const revertMessageID = createMemo(() => info()?.revert?.messageID)
|
||||||
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
||||||
const userMessages = createMemo(() =>
|
const emptyUserMessages: UserMessage[] = []
|
||||||
messages()
|
const userMessages = createMemo(
|
||||||
.filter((m) => m.role === "user")
|
() => messages().filter((m) => m.role === "user") as UserMessage[],
|
||||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
emptyUserMessages,
|
||||||
|
{ equals: same },
|
||||||
|
)
|
||||||
|
const visibleUserMessages = createMemo(
|
||||||
|
() => {
|
||||||
|
const revert = revertMessageID()
|
||||||
|
if (!revert) return userMessages()
|
||||||
|
return userMessages().filter((m) => m.id < revert)
|
||||||
|
},
|
||||||
|
emptyUserMessages,
|
||||||
|
{ equals: same },
|
||||||
|
)
|
||||||
|
const lastUserMessage = createMemo(() => visibleUserMessages().at(-1))
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() => lastUserMessage()?.id,
|
||||||
|
() => {
|
||||||
|
const msg = lastUserMessage()
|
||||||
|
if (!msg) return
|
||||||
|
if (msg.agent) local.agent.set(msg.agent)
|
||||||
|
if (msg.model) local.model.set(msg.model)
|
||||||
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
const visibleUserMessages = createMemo(() => {
|
|
||||||
const revert = revertMessageID()
|
|
||||||
if (!revert) return userMessages()
|
|
||||||
return userMessages().filter((m) => m.id < revert)
|
|
||||||
})
|
|
||||||
const lastUserMessage = createMemo(() => visibleUserMessages()?.at(-1))
|
|
||||||
|
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
clickTimer: undefined as number | undefined,
|
clickTimer: undefined as number | undefined,
|
||||||
@@ -155,16 +185,37 @@ export default function Page() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
createEffect(() => {
|
const idle = { type: "idle" as const }
|
||||||
params.id
|
|
||||||
const status = sync.data.session_status[params.id ?? ""] ?? { type: "idle" }
|
createEffect(
|
||||||
batch(() => {
|
on(
|
||||||
setStore("userInteracted", false)
|
() => params.id,
|
||||||
setStore("stepsExpanded", status.type !== "idle")
|
(id) => {
|
||||||
})
|
const status = sync.data.session_status[id ?? ""] ?? idle
|
||||||
})
|
batch(() => {
|
||||||
|
setStore("userInteracted", false)
|
||||||
|
setStore("stepsExpanded", status.type !== "idle")
|
||||||
|
})
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? idle)
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() => status().type,
|
||||||
|
(type) => {
|
||||||
|
if (type !== "idle") return
|
||||||
|
batch(() => {
|
||||||
|
setStore("userInteracted", false)
|
||||||
|
setStore("stepsExpanded", false)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ defer: true },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? { type: "idle" })
|
|
||||||
const working = createMemo(() => status().type !== "idle" && activeMessage()?.id === lastUserMessage()?.id)
|
const working = createMemo(() => status().type !== "idle" && activeMessage()?.id === lastUserMessage()?.id)
|
||||||
|
|
||||||
createRenderEffect((prev) => {
|
createRenderEffect((prev) => {
|
||||||
@@ -274,6 +325,15 @@ export default function Page() {
|
|||||||
slash: "model",
|
slash: "model",
|
||||||
onSelect: () => dialog.show(() => <DialogSelectModel />),
|
onSelect: () => dialog.show(() => <DialogSelectModel />),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "mcp.toggle",
|
||||||
|
title: "Toggle MCPs",
|
||||||
|
description: "Toggle MCPs",
|
||||||
|
category: "MCP",
|
||||||
|
keybind: "mod+;",
|
||||||
|
slash: "mcp",
|
||||||
|
onSelect: () => dialog.show(() => <DialogSelectMcp />),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "agent.cycle",
|
id: "agent.cycle",
|
||||||
title: "Cycle agent",
|
title: "Cycle agent",
|
||||||
@@ -291,6 +351,22 @@ export default function Page() {
|
|||||||
keybind: "shift+mod+.",
|
keybind: "shift+mod+.",
|
||||||
onSelect: () => local.agent.move(-1),
|
onSelect: () => local.agent.move(-1),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "permissions.autoaccept",
|
||||||
|
title: params.id && permission.isAutoAccepting(params.id) ? "Stop auto-accepting edits" : "Auto-accept edits",
|
||||||
|
category: "Permissions",
|
||||||
|
disabled: !params.id,
|
||||||
|
onSelect: () => {
|
||||||
|
if (!params.id) return
|
||||||
|
permission.toggleAutoAccept(params.id)
|
||||||
|
showToast({
|
||||||
|
title: permission.isAutoAccepting(params.id) ? "Auto-accepting edits" : "Stopped auto-accepting edits",
|
||||||
|
description: permission.isAutoAccepting(params.id)
|
||||||
|
? "Edit and write permissions will be automatically approved"
|
||||||
|
: "Edit and write permissions will require approval",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "session.undo",
|
id: "session.undo",
|
||||||
title: "Undo",
|
title: "Undo",
|
||||||
@@ -562,6 +638,7 @@ export default function Page() {
|
|||||||
<SessionTurn
|
<SessionTurn
|
||||||
sessionID={params.id!}
|
sessionID={params.id!}
|
||||||
messageID={message.id}
|
messageID={message.id}
|
||||||
|
lastUserMessageID={lastUserMessage()?.id}
|
||||||
stepsExpanded={store.mobileStepsExpanded[message.id] ?? false}
|
stepsExpanded={store.mobileStepsExpanded[message.id] ?? false}
|
||||||
onStepsExpandedToggle={() => setStore("mobileStepsExpanded", message.id, (x) => !x)}
|
onStepsExpandedToggle={() => setStore("mobileStepsExpanded", message.id, (x) => !x)}
|
||||||
onUserInteracted={() => setStore("userInteracted", true)}
|
onUserInteracted={() => setStore("userInteracted", true)}
|
||||||
@@ -618,6 +695,7 @@ export default function Page() {
|
|||||||
<SessionTurn
|
<SessionTurn
|
||||||
sessionID={params.id!}
|
sessionID={params.id!}
|
||||||
messageID={activeMessage()!.id}
|
messageID={activeMessage()!.id}
|
||||||
|
lastUserMessageID={lastUserMessage()?.id}
|
||||||
stepsExpanded={store.stepsExpanded}
|
stepsExpanded={store.stepsExpanded}
|
||||||
onStepsExpandedToggle={() => setStore("stepsExpanded", (x) => !x)}
|
onStepsExpandedToggle={() => setStore("stepsExpanded", (x) => !x)}
|
||||||
onUserInteracted={() => setStore("userInteracted", true)}
|
onUserInteracted={() => setStore("userInteracted", true)}
|
||||||
@@ -921,6 +999,10 @@ export default function Page() {
|
|||||||
</DragDropProvider>
|
</DragDropProvider>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
<StatusBar>
|
||||||
|
<SessionLspIndicator />
|
||||||
|
<SessionMcpIndicator />
|
||||||
|
</StatusBar>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.0.199",
|
"version": "1.0.209",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo --noEmit",
|
||||||
|
|||||||
@@ -5,28 +5,36 @@ import { useAuthSession } from "~/context/auth.session"
|
|||||||
|
|
||||||
export async function GET(input: APIEvent) {
|
export async function GET(input: APIEvent) {
|
||||||
const url = new URL(input.request.url)
|
const url = new URL(input.request.url)
|
||||||
const code = url.searchParams.get("code")
|
try {
|
||||||
if (!code) throw new Error("No code found")
|
const code = url.searchParams.get("code")
|
||||||
const result = await AuthClient.exchange(code, `${url.origin}${url.pathname}`)
|
if (!code) throw new Error("No code found")
|
||||||
if (result.err) {
|
const result = await AuthClient.exchange(code, `${url.origin}${url.pathname}`)
|
||||||
throw new Error(result.err.message)
|
if (result.err) throw new Error(result.err.message)
|
||||||
}
|
const decoded = AuthClient.decode(result.tokens.access, {} as any)
|
||||||
const decoded = AuthClient.decode(result.tokens.access, {} as any)
|
if (decoded.err) throw new Error(decoded.err.message)
|
||||||
if (decoded.err) throw new Error(decoded.err.message)
|
const session = await useAuthSession()
|
||||||
const session = await useAuthSession()
|
const id = decoded.subject.properties.accountID
|
||||||
const id = decoded.subject.properties.accountID
|
await session.update((value) => {
|
||||||
await session.update((value) => {
|
return {
|
||||||
return {
|
...value,
|
||||||
...value,
|
account: {
|
||||||
account: {
|
...value.account,
|
||||||
...value.account,
|
[id]: {
|
||||||
[id]: {
|
id,
|
||||||
id,
|
email: decoded.subject.properties.email,
|
||||||
email: decoded.subject.properties.email,
|
},
|
||||||
},
|
},
|
||||||
},
|
current: id,
|
||||||
current: id,
|
}
|
||||||
}
|
})
|
||||||
})
|
return redirect("/auth")
|
||||||
return redirect("/auth")
|
} catch (e: any) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
error: e.message,
|
||||||
|
cause: Object.fromEntries(url.searchParams.entries()),
|
||||||
|
}),
|
||||||
|
{ status: 500 },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,365 @@
|
|||||||
|
import { Title } from "@solidjs/meta"
|
||||||
|
import { createAsync, query, useParams } from "@solidjs/router"
|
||||||
|
import { createSignal, For, Show } from "solid-js"
|
||||||
|
import { Database, desc, eq } from "@opencode-ai/console-core/drizzle/index.js"
|
||||||
|
import { BenchmarkTable } from "@opencode-ai/console-core/schema/benchmark.sql.js"
|
||||||
|
|
||||||
|
interface TaskSource {
|
||||||
|
repo: string
|
||||||
|
from: string
|
||||||
|
to: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Judge {
|
||||||
|
score: number
|
||||||
|
rationale: string
|
||||||
|
judge: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScoreDetail {
|
||||||
|
criterion: string
|
||||||
|
weight: number
|
||||||
|
average: number
|
||||||
|
variance?: number
|
||||||
|
judges?: Judge[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RunUsage {
|
||||||
|
input: number
|
||||||
|
output: number
|
||||||
|
cost: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Run {
|
||||||
|
task: string
|
||||||
|
model: string
|
||||||
|
agent: string
|
||||||
|
score: {
|
||||||
|
final: number
|
||||||
|
base: number
|
||||||
|
penalty: number
|
||||||
|
}
|
||||||
|
scoreDetails: ScoreDetail[]
|
||||||
|
usage?: RunUsage
|
||||||
|
duration?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Prompt {
|
||||||
|
commit: string
|
||||||
|
prompt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AverageUsage {
|
||||||
|
input: number
|
||||||
|
output: number
|
||||||
|
cost: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Task {
|
||||||
|
averageScore: number
|
||||||
|
averageDuration?: number
|
||||||
|
averageUsage?: AverageUsage
|
||||||
|
model?: string
|
||||||
|
agent?: string
|
||||||
|
summary?: string
|
||||||
|
runs?: Run[]
|
||||||
|
task: {
|
||||||
|
id: string
|
||||||
|
source: TaskSource
|
||||||
|
prompts?: Prompt[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BenchmarkResult {
|
||||||
|
averageScore: number
|
||||||
|
tasks: Task[]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTaskDetail(benchmarkId: string, taskId: string) {
|
||||||
|
"use server"
|
||||||
|
const rows = await Database.use((tx) =>
|
||||||
|
tx.select().from(BenchmarkTable).where(eq(BenchmarkTable.id, benchmarkId)).limit(1),
|
||||||
|
)
|
||||||
|
if (!rows[0]) return null
|
||||||
|
const parsed = JSON.parse(rows[0].result) as BenchmarkResult
|
||||||
|
const task = parsed.tasks.find((t) => t.task.id === taskId)
|
||||||
|
return task ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryTaskDetail = query(getTaskDetail, "benchmark.task.detail")
|
||||||
|
|
||||||
|
function formatDuration(ms: number): string {
|
||||||
|
const seconds = Math.floor(ms / 1000)
|
||||||
|
const minutes = Math.floor(seconds / 60)
|
||||||
|
const remainingSeconds = seconds % 60
|
||||||
|
if (minutes > 0) {
|
||||||
|
return `${minutes}m ${remainingSeconds}s`
|
||||||
|
}
|
||||||
|
return `${remainingSeconds}s`
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BenchDetail() {
|
||||||
|
const params = useParams()
|
||||||
|
const [benchmarkId, taskId] = (params.id ?? "").split(":")
|
||||||
|
const task = createAsync(() => queryTaskDetail(benchmarkId, taskId))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main data-page="bench-detail">
|
||||||
|
<Title>Benchmark - {taskId}</Title>
|
||||||
|
<div style={{ padding: "1rem" }}>
|
||||||
|
<Show when={task()} fallback={<p>Task not found</p>}>
|
||||||
|
<div style={{ "margin-bottom": "1rem" }}>
|
||||||
|
<div>
|
||||||
|
<strong>Agent: </strong>
|
||||||
|
{task()?.agent ?? "N/A"}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Model: </strong>
|
||||||
|
{task()?.model ?? "N/A"}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Task: </strong>
|
||||||
|
{task()!.task.id}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ "margin-bottom": "1rem" }}>
|
||||||
|
<div>
|
||||||
|
<strong>Repo: </strong>
|
||||||
|
<a
|
||||||
|
href={`https://github.com/${task()!.task.source.repo}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ color: "#0066cc" }}
|
||||||
|
>
|
||||||
|
{task()!.task.source.repo}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>From: </strong>
|
||||||
|
<a
|
||||||
|
href={`https://github.com/${task()!.task.source.repo}/commit/${task()!.task.source.from}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ color: "#0066cc" }}
|
||||||
|
>
|
||||||
|
{task()!.task.source.from.slice(0, 7)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>To: </strong>
|
||||||
|
<a
|
||||||
|
href={`https://github.com/${task()!.task.source.repo}/commit/${task()!.task.source.to}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ color: "#0066cc" }}
|
||||||
|
>
|
||||||
|
{task()!.task.source.to.slice(0, 7)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={task()?.task.prompts && task()!.task.prompts!.length > 0}>
|
||||||
|
<div style={{ "margin-bottom": "1rem" }}>
|
||||||
|
<strong>Prompt:</strong>
|
||||||
|
<For each={task()!.task.prompts}>
|
||||||
|
{(p) => (
|
||||||
|
<div style={{ "margin-top": "0.5rem" }}>
|
||||||
|
<div style={{ "font-size": "0.875rem", color: "#666" }}>Commit: {p.commit.slice(0, 7)}</div>
|
||||||
|
<p style={{ "margin-top": "0.25rem", "white-space": "pre-wrap" }}>{p.prompt}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<hr style={{ margin: "1rem 0", border: "none", "border-top": "1px solid #ccc" }} />
|
||||||
|
|
||||||
|
<div style={{ "margin-bottom": "1rem" }}>
|
||||||
|
<div>
|
||||||
|
<strong>Average Duration: </strong>
|
||||||
|
{task()?.averageDuration ? formatDuration(task()!.averageDuration!) : "N/A"}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Average Score: </strong>
|
||||||
|
{task()?.averageScore?.toFixed(3) ?? "N/A"}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Average Cost: </strong>
|
||||||
|
{task()?.averageUsage?.cost ? `$${task()!.averageUsage!.cost.toFixed(4)}` : "N/A"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={task()?.summary}>
|
||||||
|
<div style={{ "margin-bottom": "1rem" }}>
|
||||||
|
<strong>Summary:</strong>
|
||||||
|
<p style={{ "margin-top": "0.5rem", "white-space": "pre-wrap" }}>{task()!.summary}</p>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={task()?.runs && task()!.runs!.length > 0}>
|
||||||
|
<div style={{ "margin-bottom": "1rem" }}>
|
||||||
|
<strong>Runs:</strong>
|
||||||
|
<table style={{ "margin-top": "0.5rem", "border-collapse": "collapse", width: "100%" }}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={{ border: "1px solid #ccc", padding: "0.5rem", "text-align": "left" }}>Run</th>
|
||||||
|
<th
|
||||||
|
style={{
|
||||||
|
border: "1px solid #ccc",
|
||||||
|
padding: "0.5rem",
|
||||||
|
"text-align": "left",
|
||||||
|
"white-space": "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Score (Base - Penalty)
|
||||||
|
</th>
|
||||||
|
<th style={{ border: "1px solid #ccc", padding: "0.5rem", "text-align": "left" }}>Cost</th>
|
||||||
|
<th style={{ border: "1px solid #ccc", padding: "0.5rem", "text-align": "left" }}>Duration</th>
|
||||||
|
<For each={task()!.runs![0]?.scoreDetails}>
|
||||||
|
{(detail) => (
|
||||||
|
<th style={{ border: "1px solid #ccc", padding: "0.5rem", "text-align": "left" }}>
|
||||||
|
{detail.criterion} ({detail.weight})
|
||||||
|
</th>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<For each={task()!.runs}>
|
||||||
|
{(run, index) => (
|
||||||
|
<tr>
|
||||||
|
<td style={{ border: "1px solid #ccc", padding: "0.5rem" }}>{index() + 1}</td>
|
||||||
|
<td style={{ border: "1px solid #ccc", padding: "0.5rem", "white-space": "nowrap" }}>
|
||||||
|
{run.score.final.toFixed(3)} ({run.score.base.toFixed(3)} - {run.score.penalty.toFixed(3)})
|
||||||
|
</td>
|
||||||
|
<td style={{ border: "1px solid #ccc", padding: "0.5rem" }}>
|
||||||
|
{run.usage?.cost ? `$${run.usage.cost.toFixed(4)}` : "N/A"}
|
||||||
|
</td>
|
||||||
|
<td style={{ border: "1px solid #ccc", padding: "0.5rem" }}>
|
||||||
|
{run.duration ? formatDuration(run.duration) : "N/A"}
|
||||||
|
</td>
|
||||||
|
<For each={run.scoreDetails}>
|
||||||
|
{(detail) => (
|
||||||
|
<td style={{ border: "1px solid #ccc", padding: "0.5rem" }}>
|
||||||
|
<For each={detail.judges}>
|
||||||
|
{(judge) => (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: judge.score === 1 ? "green" : judge.score === 0 ? "red" : "inherit",
|
||||||
|
"margin-right": "0.25rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{judge.score === 1 ? "✓" : judge.score === 0 ? "✗" : judge.score}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<For each={task()!.runs}>
|
||||||
|
{(run, index) => (
|
||||||
|
<div style={{ "margin-top": "1rem" }}>
|
||||||
|
<h3 style={{ margin: "0 0 0.5rem 0" }}>Run {index() + 1}</h3>
|
||||||
|
<div>
|
||||||
|
<strong>Score: </strong>
|
||||||
|
{run.score.final.toFixed(3)} (Base: {run.score.base.toFixed(3)} - Penalty:{" "}
|
||||||
|
{run.score.penalty.toFixed(3)})
|
||||||
|
</div>
|
||||||
|
<For each={run.scoreDetails}>
|
||||||
|
{(detail) => (
|
||||||
|
<div style={{ "margin-top": "1rem", "padding-left": "1rem", "border-left": "2px solid #ccc" }}>
|
||||||
|
<div>
|
||||||
|
{detail.criterion} (weight: {detail.weight}){" "}
|
||||||
|
<For each={detail.judges}>
|
||||||
|
{(judge) => (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: judge.score === 1 ? "green" : judge.score === 0 ? "red" : "inherit",
|
||||||
|
"margin-right": "0.25rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{judge.score === 1 ? "✓" : judge.score === 0 ? "✗" : judge.score}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
<Show when={detail.judges && detail.judges.length > 0}>
|
||||||
|
<For each={detail.judges}>
|
||||||
|
{(judge) => {
|
||||||
|
const [expanded, setExpanded] = createSignal(false)
|
||||||
|
return (
|
||||||
|
<div style={{ "margin-top": "0.5rem", "padding-left": "1rem" }}>
|
||||||
|
<div
|
||||||
|
style={{ "font-size": "0.875rem", cursor: "pointer" }}
|
||||||
|
onClick={() => setExpanded(!expanded())}
|
||||||
|
>
|
||||||
|
<span style={{ "margin-right": "0.5rem" }}>{expanded() ? "▼" : "▶"}</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: judge.score === 1 ? "green" : judge.score === 0 ? "red" : "inherit",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{judge.score === 1 ? "✓" : judge.score === 0 ? "✗" : judge.score}
|
||||||
|
</span>{" "}
|
||||||
|
{judge.judge}
|
||||||
|
</div>
|
||||||
|
<Show when={expanded()}>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: "0.25rem 0 0 0",
|
||||||
|
"white-space": "pre-wrap",
|
||||||
|
"font-size": "0.875rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{judge.rationale}
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{(() => {
|
||||||
|
const [jsonExpanded, setJsonExpanded] = createSignal(false)
|
||||||
|
return (
|
||||||
|
<div style={{ "margin-top": "1rem" }}>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
padding: "0.75rem 1.5rem",
|
||||||
|
"font-size": "1rem",
|
||||||
|
background: "#f0f0f0",
|
||||||
|
border: "1px solid #ccc",
|
||||||
|
"border-radius": "4px",
|
||||||
|
}}
|
||||||
|
onClick={() => setJsonExpanded(!jsonExpanded())}
|
||||||
|
>
|
||||||
|
<span style={{ "margin-right": "0.5rem" }}>{jsonExpanded() ? "▼" : "▶"}</span>
|
||||||
|
Raw JSON
|
||||||
|
</button>
|
||||||
|
<Show when={jsonExpanded()}>
|
||||||
|
<pre>{JSON.stringify(task(), null, 2)}</pre>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { Title } from "@solidjs/meta"
|
||||||
|
import { A, createAsync, query } from "@solidjs/router"
|
||||||
|
import { createMemo, For, Show } from "solid-js"
|
||||||
|
import { Database, desc } from "@opencode-ai/console-core/drizzle/index.js"
|
||||||
|
import { BenchmarkTable } from "@opencode-ai/console-core/schema/benchmark.sql.js"
|
||||||
|
|
||||||
|
interface BenchmarkResult {
|
||||||
|
averageScore: number
|
||||||
|
tasks: { averageScore: number; task: { id: string } }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBenchmarks() {
|
||||||
|
"use server"
|
||||||
|
const rows = await Database.use((tx) =>
|
||||||
|
tx.select().from(BenchmarkTable).orderBy(desc(BenchmarkTable.timeCreated)).limit(100),
|
||||||
|
)
|
||||||
|
return rows.map((row) => {
|
||||||
|
const parsed = JSON.parse(row.result) as BenchmarkResult
|
||||||
|
const taskScores: Record<string, number> = {}
|
||||||
|
for (const t of parsed.tasks) {
|
||||||
|
taskScores[t.task.id] = t.averageScore
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
agent: row.agent,
|
||||||
|
model: row.model,
|
||||||
|
averageScore: parsed.averageScore,
|
||||||
|
taskScores,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryBenchmarks = query(getBenchmarks, "benchmarks.list")
|
||||||
|
|
||||||
|
export default function Bench() {
|
||||||
|
const benchmarks = createAsync(() => queryBenchmarks())
|
||||||
|
|
||||||
|
const taskIds = createMemo(() => {
|
||||||
|
const ids = new Set<string>()
|
||||||
|
for (const row of benchmarks() ?? []) {
|
||||||
|
for (const id of Object.keys(row.taskScores)) {
|
||||||
|
ids.add(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...ids].sort()
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main data-page="bench" style={{ padding: "2rem" }}>
|
||||||
|
<Title>Benchmark</Title>
|
||||||
|
<h1 style={{ "margin-bottom": "1.5rem" }}>Benchmarks</h1>
|
||||||
|
<table style={{ "border-collapse": "collapse", width: "100%" }}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={{ "text-align": "left", padding: "0.75rem" }}>Agent</th>
|
||||||
|
<th style={{ "text-align": "left", padding: "0.75rem" }}>Model</th>
|
||||||
|
<th style={{ "text-align": "left", padding: "0.75rem" }}>Score</th>
|
||||||
|
<For each={taskIds()}>{(id) => <th style={{ "text-align": "left", padding: "0.75rem" }}>{id}</th>}</For>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<For each={benchmarks()}>
|
||||||
|
{(row) => (
|
||||||
|
<tr>
|
||||||
|
<td style={{ padding: "0.75rem" }}>{row.agent}</td>
|
||||||
|
<td style={{ padding: "0.75rem" }}>{row.model}</td>
|
||||||
|
<td style={{ padding: "0.75rem" }}>{row.averageScore.toFixed(3)}</td>
|
||||||
|
<For each={taskIds()}>
|
||||||
|
{(id) => (
|
||||||
|
<td style={{ padding: "0.75rem" }}>
|
||||||
|
<Show when={row.taskScores[id] !== undefined} fallback="">
|
||||||
|
<A href={`/bench/${row.id}:${id}`} style={{ color: "#0066cc" }}>
|
||||||
|
{row.taskScores[id]?.toFixed(3)}
|
||||||
|
</A>
|
||||||
|
</Show>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type { APIEvent } from "@solidjs/start/server"
|
||||||
|
import { Database } from "@opencode-ai/console-core/drizzle/index.js"
|
||||||
|
import { BenchmarkTable } from "@opencode-ai/console-core/schema/benchmark.sql.js"
|
||||||
|
import { Identifier } from "@opencode-ai/console-core/identifier.js"
|
||||||
|
|
||||||
|
interface SubmissionBody {
|
||||||
|
model: string
|
||||||
|
agent: string
|
||||||
|
result: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(event: APIEvent) {
|
||||||
|
const body = (await event.request.json()) as SubmissionBody
|
||||||
|
|
||||||
|
if (!body.model || !body.agent || !body.result) {
|
||||||
|
return Response.json({ error: "All fields are required" }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
await Database.use((tx) =>
|
||||||
|
tx.insert(BenchmarkTable).values({
|
||||||
|
id: Identifier.create("benchmark"),
|
||||||
|
model: body.model,
|
||||||
|
agent: body.agent,
|
||||||
|
result: body.result,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response.json({ success: true }, { status: 200 })
|
||||||
|
}
|
||||||
@@ -124,6 +124,8 @@ export async function handler(
|
|||||||
res.status !== 200 &&
|
res.status !== 200 &&
|
||||||
// ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found.
|
// ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found.
|
||||||
res.status !== 404 &&
|
res.status !== 404 &&
|
||||||
|
// ie. cannot change codex model providers mid-session
|
||||||
|
!modelInfo.stickyProvider &&
|
||||||
modelInfo.fallbackProvider &&
|
modelInfo.fallbackProvider &&
|
||||||
providerInfo.id !== modelInfo.fallbackProvider
|
providerInfo.id !== modelInfo.fallbackProvider
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
CREATE TABLE `benchmark` (
|
||||||
|
`id` varchar(30) NOT NULL,
|
||||||
|
`time_created` timestamp(3) NOT NULL DEFAULT (now()),
|
||||||
|
`time_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
`time_deleted` timestamp(3),
|
||||||
|
`model` varchar(64) NOT NULL,
|
||||||
|
`agent` varchar(64) NOT NULL,
|
||||||
|
`result` mediumtext NOT NULL,
|
||||||
|
CONSTRAINT `benchmark_id_pk` PRIMARY KEY(`id`)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX `time_created` ON `benchmark` (`time_created`);
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -274,6 +274,13 @@
|
|||||||
"when": 1764110043942,
|
"when": 1764110043942,
|
||||||
"tag": "0038_famous_magik",
|
"tag": "0038_famous_magik",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 39,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1766946179892,
|
||||||
|
"tag": "0039_striped_forge",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.0.199",
|
"version": "1.0.209",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export namespace Identifier {
|
|||||||
const prefixes = {
|
const prefixes = {
|
||||||
account: "acc",
|
account: "acc",
|
||||||
auth: "aut",
|
auth: "aut",
|
||||||
|
benchmark: "ben",
|
||||||
billing: "bil",
|
billing: "bil",
|
||||||
key: "key",
|
key: "key",
|
||||||
model: "mod",
|
model: "mod",
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { index, mediumtext, mysqlTable, primaryKey, varchar } from "drizzle-orm/mysql-core"
|
||||||
|
import { id, timestamps } from "../drizzle/types"
|
||||||
|
|
||||||
|
export const BenchmarkTable = mysqlTable(
|
||||||
|
"benchmark",
|
||||||
|
{
|
||||||
|
id: id(),
|
||||||
|
...timestamps,
|
||||||
|
model: varchar("model", { length: 64 }).notNull(),
|
||||||
|
agent: varchar("agent", { length: 64 }).notNull(),
|
||||||
|
result: mediumtext("result").notNull(),
|
||||||
|
},
|
||||||
|
(table) => [primaryKey({ columns: [table.id] }), index("time_created").on(table.timeCreated)],
|
||||||
|
)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.0.199",
|
"version": "1.0.209",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -123,7 +123,11 @@ export default {
|
|||||||
},
|
},
|
||||||
}).then((x) => x.json())) as any
|
}).then((x) => x.json())) as any
|
||||||
subject = user.id.toString()
|
subject = user.id.toString()
|
||||||
email = emails.find((x: any) => x.primary && x.verified)?.email
|
|
||||||
|
const primaryEmail = emails.find((x: any) => x.primary)
|
||||||
|
if (!primaryEmail) throw new Error("No primary email found for GitHub user")
|
||||||
|
if (!primaryEmail.verified) throw new Error("Primary email for GitHub user not verified")
|
||||||
|
email = primaryEmail.email
|
||||||
} else if (response.provider === "google") {
|
} else if (response.provider === "google") {
|
||||||
if (!response.id.email_verified) throw new Error("Google email not verified")
|
if (!response.id.email_verified) throw new Error("Google email not verified")
|
||||||
subject = response.id.sub as string
|
subject = response.id.sub as string
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.0.199",
|
"version": "1.0.209",
|
||||||
"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",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en" style="background-color: var(--background-base)">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
@@ -13,14 +13,39 @@
|
|||||||
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
|
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
|
||||||
<meta property="og:image" content="/social-share.png" />
|
<meta property="og:image" content="/social-share.png" />
|
||||||
<meta property="twitter:image" content="/social-share.png" />
|
<meta property="twitter:image" content="/social-share.png" />
|
||||||
</head>
|
<!-- Theme preload script - applies cached theme to avoid FOUC -->
|
||||||
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
<script id="oc-theme-preload-script">
|
||||||
<script>
|
|
||||||
;(function () {
|
;(function () {
|
||||||
const savedTheme = localStorage.getItem("theme") || "oc-1"
|
var themeId = localStorage.getItem("opencode-theme-id")
|
||||||
document.documentElement.setAttribute("data-theme", savedTheme)
|
if (!themeId) return
|
||||||
|
|
||||||
|
var scheme = localStorage.getItem("opencode-color-scheme") || "system"
|
||||||
|
var isDark = scheme === "dark" || (scheme === "system" && matchMedia("(prefers-color-scheme: dark)").matches)
|
||||||
|
var mode = isDark ? "dark" : "light"
|
||||||
|
|
||||||
|
document.documentElement.dataset.theme = themeId
|
||||||
|
document.documentElement.dataset.colorScheme = mode
|
||||||
|
|
||||||
|
if (themeId === "oc-1") return
|
||||||
|
|
||||||
|
var css = localStorage.getItem("opencode-theme-css-" + themeId + "-" + mode)
|
||||||
|
if (css) {
|
||||||
|
var style = document.createElement("style")
|
||||||
|
style.id = "oc-theme-preload"
|
||||||
|
style.textContent =
|
||||||
|
":root{color-scheme:" +
|
||||||
|
mode +
|
||||||
|
";--text-mix-blend-mode:" +
|
||||||
|
(isDark ? "plus-lighter" : "multiply") +
|
||||||
|
";" +
|
||||||
|
css +
|
||||||
|
"}"
|
||||||
|
document.head.appendChild(style)
|
||||||
|
}
|
||||||
})()
|
})()
|
||||||
</script>
|
</script>
|
||||||
|
</head>
|
||||||
|
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root" class="flex flex-col h-screen"></div>
|
<div id="root" class="flex flex-col h-screen"></div>
|
||||||
<script src="/src/index.tsx" type="module"></script>
|
<script src="/src/index.tsx" type="module"></script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.199",
|
"version": "1.0.209",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo -b",
|
"typecheck": "tsgo -b",
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ export default defineConfig({
|
|||||||
//
|
//
|
||||||
// 1. prevent Vite from obscuring rust errors
|
// 1. prevent Vite from obscuring rust errors
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
|
esbuild: {
|
||||||
|
// Improves production stack traces (less "kQ@..." noise)
|
||||||
|
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,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.0.199",
|
"version": "1.0.209",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -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.199"
|
version = "1.0.209"
|
||||||
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.199/opencode-darwin-arm64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.209/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.199/opencode-darwin-x64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.209/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.199/opencode-linux-arm64.tar.gz"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.209/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.199/opencode-linux-x64.tar.gz"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.209/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.199/opencode-windows-x64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.209/opencode-windows-x64.zip"
|
||||||
cmd = "./opencode.exe"
|
cmd = "./opencode.exe"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.0.199",
|
"version": "1.0.209",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -2,3 +2,5 @@ preload = ["@opentui/solid/preload"]
|
|||||||
|
|
||||||
[test]
|
[test]
|
||||||
preload = ["./test/preload.ts"]
|
preload = ["./test/preload.ts"]
|
||||||
|
# Enable code coverage
|
||||||
|
coverage = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.0.199",
|
"version": "1.0.209",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -50,16 +50,23 @@
|
|||||||
"@actions/github": "6.0.1",
|
"@actions/github": "6.0.1",
|
||||||
"@agentclientprotocol/sdk": "0.5.1",
|
"@agentclientprotocol/sdk": "0.5.1",
|
||||||
"@ai-sdk/amazon-bedrock": "3.0.57",
|
"@ai-sdk/amazon-bedrock": "3.0.57",
|
||||||
"@ai-sdk/anthropic": "2.0.50",
|
"@ai-sdk/anthropic": "2.0.56",
|
||||||
"@ai-sdk/azure": "2.0.73",
|
"@ai-sdk/azure": "2.0.73",
|
||||||
|
"@ai-sdk/cerebras": "1.0.33",
|
||||||
|
"@ai-sdk/cohere": "2.0.21",
|
||||||
|
"@ai-sdk/deepinfra": "1.0.30",
|
||||||
|
"@ai-sdk/gateway": "2.0.23",
|
||||||
"@ai-sdk/google": "2.0.44",
|
"@ai-sdk/google": "2.0.44",
|
||||||
"@ai-sdk/google-vertex": "3.0.81",
|
"@ai-sdk/google-vertex": "3.0.81",
|
||||||
|
"@ai-sdk/groq": "2.0.33",
|
||||||
"@ai-sdk/mcp": "0.0.8",
|
"@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.27",
|
||||||
|
"@ai-sdk/perplexity": "2.0.22",
|
||||||
"@ai-sdk/provider": "2.0.0",
|
"@ai-sdk/provider": "2.0.0",
|
||||||
"@ai-sdk/provider-utils": "3.0.18",
|
"@ai-sdk/provider-utils": "3.0.19",
|
||||||
|
"@ai-sdk/togetherai": "1.0.30",
|
||||||
"@ai-sdk/xai": "2.0.42",
|
"@ai-sdk/xai": "2.0.42",
|
||||||
"@clack/prompts": "1.0.0-alpha.1",
|
"@clack/prompts": "1.0.0-alpha.1",
|
||||||
"@hono/standard-validator": "0.1.5",
|
"@hono/standard-validator": "0.1.5",
|
||||||
@@ -81,6 +88,7 @@
|
|||||||
"@standard-schema/spec": "1.0.0",
|
"@standard-schema/spec": "1.0.0",
|
||||||
"@zip.js/zip.js": "2.7.62",
|
"@zip.js/zip.js": "2.7.62",
|
||||||
"ai": "catalog:",
|
"ai": "catalog:",
|
||||||
|
"bonjour-service": "1.3.0",
|
||||||
"bun-pty": "0.4.2",
|
"bun-pty": "0.4.2",
|
||||||
"chokidar": "4.0.3",
|
"chokidar": "4.0.3",
|
||||||
"clipboardy": "4.0.0",
|
"clipboardy": "4.0.0",
|
||||||
|
|||||||
@@ -235,5 +235,19 @@ export default {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
filetype: "nix",
|
||||||
|
// TODO: Replace with official tree-sitter-nix WASM when published
|
||||||
|
// See: https://github.com/nix-community/tree-sitter-nix/issues/66
|
||||||
|
wasm: "https://github.com/ast-grep/ast-grep.github.io/raw/40b84530640aa83a0d34a20a2b0623d7b8e5ea97/website/public/parsers/tree-sitter-nix.wasm",
|
||||||
|
queries: {
|
||||||
|
highlights: [
|
||||||
|
"https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/nix/highlights.scm",
|
||||||
|
],
|
||||||
|
locals: [
|
||||||
|
"https://raw.githubusercontent.com/nvim-treesitter/nvim-treesitter/refs/heads/master/queries/nix/locals.scm",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ Rules:
|
|||||||
- Do not explain what the user asked for
|
- Do not explain what the user asked for
|
||||||
- Write in first person (I added..., I fixed...)
|
- Write in first person (I added..., I fixed...)
|
||||||
- Never ask questions or add new questions
|
- Never ask questions or add new questions
|
||||||
- Only exception: if the conversation ends with an unanswered question to the user, preserve that exact question
|
- If the conversation ends with an unanswered question to the user, preserve that exact question
|
||||||
|
- If the conversation ends with an imperative statement or request to the user (e.g. "Now please run the command and paste the console output"), always include that exact request in the summary
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Your output must be:
|
|||||||
- The title should NEVER include "summarizing" or "generating" when generating a title
|
- The title should NEVER include "summarizing" or "generating" when generating a title
|
||||||
- DO NOT SAY YOU CANNOT GENERATE A TITLE OR COMPLAIN ABOUT THE INPUT
|
- DO NOT SAY YOU CANNOT GENERATE A TITLE OR COMPLAIN ABOUT THE INPUT
|
||||||
- Always output something meaningful, even if the input is minimal.
|
- Always output something meaningful, even if the input is minimal.
|
||||||
- If the user message is short or conversational (e.g. "hello", "lol", "whats up", "hey"):
|
- If the user message is short or conversational (e.g. "hello", "lol", "what's up", "hey"):
|
||||||
→ create a title that reflects the user's tone or intent (such as Greeting, Quick check-in, Light chat, Intro message, etc.)
|
→ create a title that reflects the user's tone or intent (such as Greeting, Quick check-in, Light chat, Intro message, etc.)
|
||||||
</rules>
|
</rules>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"
|
|||||||
import { ACP } from "@/acp/agent"
|
import { ACP } from "@/acp/agent"
|
||||||
import { Server } from "@/server/server"
|
import { Server } from "@/server/server"
|
||||||
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2"
|
||||||
|
import { withNetworkOptions, resolveNetworkOptions } from "../network"
|
||||||
|
|
||||||
const log = Log.create({ service: "acp-command" })
|
const log = Log.create({ service: "acp-command" })
|
||||||
|
|
||||||
@@ -19,29 +20,16 @@ export const AcpCommand = cmd({
|
|||||||
command: "acp",
|
command: "acp",
|
||||||
describe: "start ACP (Agent Client Protocol) server",
|
describe: "start ACP (Agent Client Protocol) server",
|
||||||
builder: (yargs) => {
|
builder: (yargs) => {
|
||||||
return yargs
|
return withNetworkOptions(yargs).option("cwd", {
|
||||||
.option("cwd", {
|
describe: "working directory",
|
||||||
describe: "working directory",
|
type: "string",
|
||||||
type: "string",
|
default: process.cwd(),
|
||||||
default: process.cwd(),
|
})
|
||||||
})
|
|
||||||
.option("port", {
|
|
||||||
type: "number",
|
|
||||||
describe: "port to listen on",
|
|
||||||
default: 0,
|
|
||||||
})
|
|
||||||
.option("hostname", {
|
|
||||||
type: "string",
|
|
||||||
describe: "hostname to listen on",
|
|
||||||
default: "127.0.0.1",
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
handler: async (args) => {
|
handler: async (args) => {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const server = Server.listen({
|
const opts = await resolveNetworkOptions(args)
|
||||||
port: args.port,
|
const server = Server.listen(opts)
|
||||||
hostname: args.hostname,
|
|
||||||
})
|
|
||||||
|
|
||||||
const sdk = createOpencodeClient({
|
const sdk = createOpencodeClient({
|
||||||
baseUrl: `http://${server.hostname}:${server.port}`,
|
baseUrl: `http://${server.hostname}:${server.port}`,
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import * as github from "@actions/github"
|
|||||||
import type { Context } from "@actions/github/lib/context"
|
import type { Context } from "@actions/github/lib/context"
|
||||||
import type {
|
import type {
|
||||||
IssueCommentEvent,
|
IssueCommentEvent,
|
||||||
|
IssuesEvent,
|
||||||
PullRequestReviewCommentEvent,
|
PullRequestReviewCommentEvent,
|
||||||
|
WorkflowDispatchEvent,
|
||||||
WorkflowRunEvent,
|
WorkflowRunEvent,
|
||||||
PullRequestEvent,
|
PullRequestEvent,
|
||||||
} from "@octokit/webhooks-types"
|
} from "@octokit/webhooks-types"
|
||||||
@@ -132,7 +134,16 @@ type IssueQueryResponse = {
|
|||||||
const AGENT_USERNAME = "opencode-agent[bot]"
|
const AGENT_USERNAME = "opencode-agent[bot]"
|
||||||
const AGENT_REACTION = "eyes"
|
const AGENT_REACTION = "eyes"
|
||||||
const WORKFLOW_FILE = ".github/workflows/opencode.yml"
|
const WORKFLOW_FILE = ".github/workflows/opencode.yml"
|
||||||
const SUPPORTED_EVENTS = ["issue_comment", "pull_request_review_comment", "schedule", "pull_request"] as const
|
|
||||||
|
// Event categories for routing
|
||||||
|
// USER_EVENTS: triggered by user actions, have actor/issueId, support reactions/comments
|
||||||
|
// REPO_EVENTS: triggered by automation, no actor/issueId, output to logs/PR only
|
||||||
|
const USER_EVENTS = ["issue_comment", "pull_request_review_comment", "issues", "pull_request"] as const
|
||||||
|
const REPO_EVENTS = ["schedule", "workflow_dispatch"] as const
|
||||||
|
const SUPPORTED_EVENTS = [...USER_EVENTS, ...REPO_EVENTS] as const
|
||||||
|
|
||||||
|
type UserEvent = (typeof USER_EVENTS)[number]
|
||||||
|
type RepoEvent = (typeof REPO_EVENTS)[number]
|
||||||
|
|
||||||
// Parses GitHub remote URLs in various formats:
|
// Parses GitHub remote URLs in various formats:
|
||||||
// - https://github.com/owner/repo.git
|
// - https://github.com/owner/repo.git
|
||||||
@@ -147,6 +158,29 @@ export function parseGitHubRemote(url: string): { owner: string; repo: string }
|
|||||||
return { owner: match[1], repo: match[2] }
|
return { owner: match[1], repo: match[2] }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts displayable text from assistant response parts.
|
||||||
|
* Returns null for tool-only or reasoning-only responses (signals summary needed).
|
||||||
|
* Throws for truly unusable responses (empty, step-start only, etc.).
|
||||||
|
*/
|
||||||
|
export function extractResponseText(parts: MessageV2.Part[]): string | null {
|
||||||
|
// Priority 1: Look for text parts
|
||||||
|
const textPart = parts.findLast((p) => p.type === "text")
|
||||||
|
if (textPart) return textPart.text
|
||||||
|
|
||||||
|
// Priority 2: Reasoning-only - return null to signal summary needed
|
||||||
|
const reasoningPart = parts.findLast((p) => p.type === "reasoning")
|
||||||
|
if (reasoningPart) return null
|
||||||
|
|
||||||
|
// Priority 3: Tool-only - return null to signal summary needed
|
||||||
|
const toolParts = parts.filter((p) => p.type === "tool" && p.state.status === "completed")
|
||||||
|
if (toolParts.length > 0) return null
|
||||||
|
|
||||||
|
// No usable parts - throw with debug info
|
||||||
|
const partTypes = parts.map((p) => p.type).join(", ") || "none"
|
||||||
|
throw new Error(`Failed to parse response. Part types found: [${partTypes}]`)
|
||||||
|
}
|
||||||
|
|
||||||
export const GithubCommand = cmd({
|
export const GithubCommand = cmd({
|
||||||
command: "github",
|
command: "github",
|
||||||
describe: "manage GitHub agent",
|
describe: "manage GitHub agent",
|
||||||
@@ -397,27 +431,38 @@ export const GithubRunCommand = cmd({
|
|||||||
core.setFailed(`Unsupported event type: ${context.eventName}`)
|
core.setFailed(`Unsupported event type: ${context.eventName}`)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine event category for routing
|
||||||
|
// USER_EVENTS: have actor, issueId, support reactions/comments
|
||||||
|
// REPO_EVENTS: no actor/issueId, output to logs/PR only
|
||||||
|
const isUserEvent = USER_EVENTS.includes(context.eventName as UserEvent)
|
||||||
|
const isRepoEvent = REPO_EVENTS.includes(context.eventName as RepoEvent)
|
||||||
const isCommentEvent = ["issue_comment", "pull_request_review_comment"].includes(context.eventName)
|
const isCommentEvent = ["issue_comment", "pull_request_review_comment"].includes(context.eventName)
|
||||||
|
const isIssuesEvent = context.eventName === "issues"
|
||||||
const isScheduleEvent = context.eventName === "schedule"
|
const isScheduleEvent = context.eventName === "schedule"
|
||||||
|
const isWorkflowDispatchEvent = context.eventName === "workflow_dispatch"
|
||||||
|
|
||||||
const { providerID, modelID } = normalizeModel()
|
const { providerID, modelID } = normalizeModel()
|
||||||
const runId = normalizeRunId()
|
const runId = normalizeRunId()
|
||||||
const share = normalizeShare()
|
const share = normalizeShare()
|
||||||
const oidcBaseUrl = normalizeOidcBaseUrl()
|
const oidcBaseUrl = normalizeOidcBaseUrl()
|
||||||
const { owner, repo } = context.repo
|
const { owner, repo } = context.repo
|
||||||
// For schedule events, payload has no issue/comment data
|
// For repo events (schedule, workflow_dispatch), payload has no issue/comment data
|
||||||
const payload = context.payload as
|
const payload = context.payload as
|
||||||
| IssueCommentEvent
|
| IssueCommentEvent
|
||||||
|
| IssuesEvent
|
||||||
| PullRequestReviewCommentEvent
|
| PullRequestReviewCommentEvent
|
||||||
|
| WorkflowDispatchEvent
|
||||||
| WorkflowRunEvent
|
| WorkflowRunEvent
|
||||||
| PullRequestEvent
|
| PullRequestEvent
|
||||||
const issueEvent = isIssueCommentEvent(payload) ? payload : undefined
|
const issueEvent = isIssueCommentEvent(payload) ? payload : undefined
|
||||||
|
// workflow_dispatch has an actor (the user who triggered it), schedule does not
|
||||||
const actor = isScheduleEvent ? undefined : context.actor
|
const actor = isScheduleEvent ? undefined : context.actor
|
||||||
|
|
||||||
const issueId = isScheduleEvent
|
const issueId = isRepoEvent
|
||||||
? undefined
|
? undefined
|
||||||
: context.eventName === "issue_comment"
|
: context.eventName === "issue_comment" || context.eventName === "issues"
|
||||||
? (payload as IssueCommentEvent).issue.number
|
? (payload as IssueCommentEvent | IssuesEvent).issue.number
|
||||||
: (payload as PullRequestEvent | PullRequestReviewCommentEvent).pull_request.number
|
: (payload as PullRequestEvent | PullRequestReviewCommentEvent).pull_request.number
|
||||||
const runUrl = `/${owner}/${repo}/actions/runs/${runId}`
|
const runUrl = `/${owner}/${repo}/actions/runs/${runId}`
|
||||||
const shareBaseUrl = isMock ? "https://dev.opencode.ai" : "https://opencode.ai"
|
const shareBaseUrl = isMock ? "https://dev.opencode.ai" : "https://opencode.ai"
|
||||||
@@ -462,8 +507,8 @@ export const GithubRunCommand = cmd({
|
|||||||
if (!useGithubToken) {
|
if (!useGithubToken) {
|
||||||
await configureGit(appToken)
|
await configureGit(appToken)
|
||||||
}
|
}
|
||||||
// Skip permission check for schedule events (no actor to check)
|
// Skip permission check and reactions for repo events (no actor to check, no issue to react to)
|
||||||
if (!isScheduleEvent) {
|
if (isUserEvent) {
|
||||||
await assertPermissions()
|
await assertPermissions()
|
||||||
await addReaction(commentType)
|
await addReaction(commentType)
|
||||||
}
|
}
|
||||||
@@ -480,25 +525,30 @@ export const GithubRunCommand = cmd({
|
|||||||
})()
|
})()
|
||||||
console.log("opencode session", session.id)
|
console.log("opencode session", session.id)
|
||||||
|
|
||||||
// Handle 4 cases
|
// Handle event types:
|
||||||
// 1. Schedule (no issue/PR context)
|
// REPO_EVENTS (schedule, workflow_dispatch): no issue/PR context, output to logs/PR only
|
||||||
// 2. Issue
|
// USER_EVENTS on PR (pull_request, pull_request_review_comment, issue_comment on PR): work on PR branch
|
||||||
// 3. Local PR
|
// USER_EVENTS on Issue (issue_comment on issue, issues): create new branch, may create PR
|
||||||
// 4. Fork PR
|
if (isRepoEvent) {
|
||||||
if (isScheduleEvent) {
|
// Repo event - no issue/PR context, output goes to logs
|
||||||
// Schedule event - no issue/PR context, output goes to logs
|
if (isWorkflowDispatchEvent && actor) {
|
||||||
const branch = await checkoutNewBranch("schedule")
|
console.log(`Triggered by: ${actor}`)
|
||||||
|
}
|
||||||
|
const branchPrefix = isWorkflowDispatchEvent ? "dispatch" : "schedule"
|
||||||
|
const branch = await checkoutNewBranch(branchPrefix)
|
||||||
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
|
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
|
||||||
const response = await chat(userPrompt, promptFiles)
|
const response = await chat(userPrompt, promptFiles)
|
||||||
const { dirty, uncommittedChanges } = await branchIsDirty(head)
|
const { dirty, uncommittedChanges } = await branchIsDirty(head)
|
||||||
if (dirty) {
|
if (dirty) {
|
||||||
const summary = await summarize(response)
|
const summary = await summarize(response)
|
||||||
await pushToNewBranch(summary, branch, uncommittedChanges, true)
|
// workflow_dispatch has an actor for co-author attribution, schedule does not
|
||||||
|
await pushToNewBranch(summary, branch, uncommittedChanges, isScheduleEvent)
|
||||||
|
const triggerType = isWorkflowDispatchEvent ? "workflow_dispatch" : "scheduled workflow"
|
||||||
const pr = await createPR(
|
const pr = await createPR(
|
||||||
repoData.data.default_branch,
|
repoData.data.default_branch,
|
||||||
branch,
|
branch,
|
||||||
summary,
|
summary,
|
||||||
`${response}\n\nTriggered by scheduled workflow${footer({ image: true })}`,
|
`${response}\n\nTriggered by ${triggerType}${footer({ image: true })}`,
|
||||||
)
|
)
|
||||||
console.log(`Created PR #${pr}`)
|
console.log(`Created PR #${pr}`)
|
||||||
} else {
|
} else {
|
||||||
@@ -573,7 +623,7 @@ export const GithubRunCommand = cmd({
|
|||||||
} else if (e instanceof Error) {
|
} else if (e instanceof Error) {
|
||||||
msg = e.message
|
msg = e.message
|
||||||
}
|
}
|
||||||
if (!isScheduleEvent) {
|
if (isUserEvent) {
|
||||||
await createComment(`${msg}${footer()}`)
|
await createComment(`${msg}${footer()}`)
|
||||||
await removeReaction(commentType)
|
await removeReaction(commentType)
|
||||||
}
|
}
|
||||||
@@ -628,9 +678,15 @@ export const GithubRunCommand = cmd({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isIssueCommentEvent(
|
function isIssueCommentEvent(
|
||||||
event: IssueCommentEvent | PullRequestReviewCommentEvent | WorkflowRunEvent | PullRequestEvent,
|
event:
|
||||||
|
| IssueCommentEvent
|
||||||
|
| IssuesEvent
|
||||||
|
| PullRequestReviewCommentEvent
|
||||||
|
| WorkflowDispatchEvent
|
||||||
|
| WorkflowRunEvent
|
||||||
|
| PullRequestEvent,
|
||||||
): event is IssueCommentEvent {
|
): event is IssueCommentEvent {
|
||||||
return "issue" in event
|
return "issue" in event && "comment" in event
|
||||||
}
|
}
|
||||||
|
|
||||||
function getReviewCommentContext() {
|
function getReviewCommentContext() {
|
||||||
@@ -652,10 +708,11 @@ export const GithubRunCommand = cmd({
|
|||||||
|
|
||||||
async function getUserPrompt() {
|
async function getUserPrompt() {
|
||||||
const customPrompt = process.env["PROMPT"]
|
const customPrompt = process.env["PROMPT"]
|
||||||
// For schedule events, PROMPT is required since there's no comment to extract from
|
// For repo events and issues events, PROMPT is required since there's no comment to extract from
|
||||||
if (isScheduleEvent) {
|
if (isRepoEvent || isIssuesEvent) {
|
||||||
if (!customPrompt) {
|
if (!customPrompt) {
|
||||||
throw new Error("PROMPT input is required for scheduled events")
|
const eventType = isRepoEvent ? "scheduled and workflow_dispatch" : "issues"
|
||||||
|
throw new Error(`PROMPT input is required for ${eventType} events`)
|
||||||
}
|
}
|
||||||
return { userPrompt: customPrompt, promptFiles: [] }
|
return { userPrompt: customPrompt, promptFiles: [] }
|
||||||
}
|
}
|
||||||
@@ -856,10 +913,41 @@ export const GithubRunCommand = cmd({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const match = result.parts.findLast((p) => p.type === "text")
|
const text = extractResponseText(result.parts)
|
||||||
if (!match) throw new Error("Failed to parse the text response")
|
if (text) return text
|
||||||
|
|
||||||
return match.text
|
// No text part (tool-only or reasoning-only) - ask agent to summarize
|
||||||
|
console.log("Requesting summary from agent...")
|
||||||
|
const summary = await SessionPrompt.prompt({
|
||||||
|
sessionID: session.id,
|
||||||
|
messageID: Identifier.ascending("message"),
|
||||||
|
model: {
|
||||||
|
providerID,
|
||||||
|
modelID,
|
||||||
|
},
|
||||||
|
tools: { "*": false }, // Disable all tools to force text response
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
id: Identifier.ascending("part"),
|
||||||
|
type: "text",
|
||||||
|
text: "Summarize the actions (tool calls & reasoning) you did for the user in 1-2 sentences.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (summary.info.role === "assistant" && summary.info.error) {
|
||||||
|
console.error(summary.info)
|
||||||
|
throw new Error(
|
||||||
|
`${summary.info.error.name}: ${"message" in summary.info.error ? summary.info.error.message : ""}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const summaryText = extractResponseText(summary.parts)
|
||||||
|
if (!summaryText) {
|
||||||
|
throw new Error("Failed to get summary from agent")
|
||||||
|
}
|
||||||
|
|
||||||
|
return summaryText
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getOidcToken() {
|
async function getOidcToken() {
|
||||||
@@ -923,7 +1011,7 @@ export const GithubRunCommand = cmd({
|
|||||||
await $`git config --local ${config} "${gitConfig}"`
|
await $`git config --local ${config} "${gitConfig}"`
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkoutNewBranch(type: "issue" | "schedule") {
|
async function checkoutNewBranch(type: "issue" | "schedule" | "dispatch") {
|
||||||
console.log("Checking out new branch...")
|
console.log("Checking out new branch...")
|
||||||
const branch = generateBranchName(type)
|
const branch = generateBranchName(type)
|
||||||
await $`git checkout -b ${branch}`
|
await $`git checkout -b ${branch}`
|
||||||
@@ -952,16 +1040,16 @@ export const GithubRunCommand = cmd({
|
|||||||
await $`git checkout -b ${localBranch} fork/${remoteBranch}`
|
await $`git checkout -b ${localBranch} fork/${remoteBranch}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateBranchName(type: "issue" | "pr" | "schedule") {
|
function generateBranchName(type: "issue" | "pr" | "schedule" | "dispatch") {
|
||||||
const timestamp = new Date()
|
const timestamp = new Date()
|
||||||
.toISOString()
|
.toISOString()
|
||||||
.replace(/[:-]/g, "")
|
.replace(/[:-]/g, "")
|
||||||
.replace(/\.\d{3}Z/, "")
|
.replace(/\.\d{3}Z/, "")
|
||||||
.split("T")
|
.split("T")
|
||||||
.join("")
|
.join("")
|
||||||
if (type === "schedule") {
|
if (type === "schedule" || type === "dispatch") {
|
||||||
const hex = crypto.randomUUID().slice(0, 6)
|
const hex = crypto.randomUUID().slice(0, 6)
|
||||||
return `opencode/scheduled-${hex}-${timestamp}`
|
return `opencode/${type}-${hex}-${timestamp}`
|
||||||
}
|
}
|
||||||
return `opencode/${type}${issueId}-${timestamp}`
|
return `opencode/${type}${issueId}-${timestamp}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,14 @@
|
|||||||
import { Server } from "../../server/server"
|
import { Server } from "../../server/server"
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
|
import { withNetworkOptions, resolveNetworkOptions } from "../network"
|
||||||
|
|
||||||
export const ServeCommand = cmd({
|
export const ServeCommand = cmd({
|
||||||
command: "serve",
|
command: "serve",
|
||||||
builder: (yargs) =>
|
builder: (yargs) => withNetworkOptions(yargs),
|
||||||
yargs
|
|
||||||
.option("port", {
|
|
||||||
alias: ["p"],
|
|
||||||
type: "number",
|
|
||||||
describe: "port to listen on",
|
|
||||||
default: 0,
|
|
||||||
})
|
|
||||||
.option("hostname", {
|
|
||||||
type: "string",
|
|
||||||
describe: "hostname to listen on",
|
|
||||||
default: "127.0.0.1",
|
|
||||||
}),
|
|
||||||
describe: "starts a headless opencode server",
|
describe: "starts a headless opencode server",
|
||||||
handler: async (args) => {
|
handler: async (args) => {
|
||||||
const hostname = args.hostname
|
const opts = await resolveNetworkOptions(args)
|
||||||
const port = args.port
|
const server = Server.listen(opts)
|
||||||
const server = Server.listen({
|
|
||||||
port,
|
|
||||||
hostname,
|
|
||||||
})
|
|
||||||
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
|
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
|
||||||
await new Promise(() => {})
|
await new Promise(() => {})
|
||||||
await server.stop()
|
await server.stop()
|
||||||
|
|||||||
@@ -82,12 +82,21 @@ async function getAllSessions(): Promise<Session.Info[]> {
|
|||||||
return sessions
|
return sessions
|
||||||
}
|
}
|
||||||
|
|
||||||
async function aggregateSessionStats(days?: number, projectFilter?: string): Promise<SessionStats> {
|
export async function aggregateSessionStats(days?: number, projectFilter?: string): Promise<SessionStats> {
|
||||||
const sessions = await getAllSessions()
|
const sessions = await getAllSessions()
|
||||||
const DAYS_IN_SECOND = 24 * 60 * 60 * 1000
|
const MS_IN_DAY = 24 * 60 * 60 * 1000
|
||||||
const cutoffTime = days ? Date.now() - days * DAYS_IN_SECOND : 0
|
|
||||||
|
|
||||||
let filteredSessions = days ? sessions.filter((session) => session.time.updated >= cutoffTime) : sessions
|
const cutoffTime = (() => {
|
||||||
|
if (days === undefined) return 0
|
||||||
|
if (days === 0) {
|
||||||
|
const now = new Date()
|
||||||
|
now.setHours(0, 0, 0, 0)
|
||||||
|
return now.getTime()
|
||||||
|
}
|
||||||
|
return Date.now() - days * MS_IN_DAY
|
||||||
|
})()
|
||||||
|
|
||||||
|
let filteredSessions = cutoffTime > 0 ? sessions.filter((session) => session.time.updated >= cutoffTime) : sessions
|
||||||
|
|
||||||
if (projectFilter !== undefined) {
|
if (projectFilter !== undefined) {
|
||||||
if (projectFilter === "") {
|
if (projectFilter === "") {
|
||||||
@@ -198,7 +207,7 @@ async function aggregateSessionStats(days?: number, projectFilter?: string): Pro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const actualDays = Math.max(1, Math.ceil((latestTime - earliestTime) / DAYS_IN_SECOND))
|
const actualDays = Math.max(1, Math.ceil((latestTime - earliestTime) / MS_IN_DAY))
|
||||||
stats.dateRange = {
|
stats.dateRange = {
|
||||||
earliest: earliestTime,
|
earliest: earliestTime,
|
||||||
latest: latestTime,
|
latest: latestTime,
|
||||||
|
|||||||
@@ -539,7 +539,7 @@ function App() {
|
|||||||
sdk.event.on(SessionApi.Event.Error.type, (evt) => {
|
sdk.event.on(SessionApi.Event.Error.type, (evt) => {
|
||||||
const error = evt.properties.error
|
const error = evt.properties.error
|
||||||
const message = (() => {
|
const message = (() => {
|
||||||
if (!error) return "An error occured"
|
if (!error) return "An error occurred"
|
||||||
|
|
||||||
if (typeof error === "object") {
|
if (typeof error === "object") {
|
||||||
const data = error.data
|
const data = error.data
|
||||||
|
|||||||
@@ -37,11 +37,9 @@ export function DialogModel(props: { providerID?: string }) {
|
|||||||
const recents = local.model.recent()
|
const recents = local.model.recent()
|
||||||
|
|
||||||
const recentList = showExtra()
|
const recentList = showExtra()
|
||||||
? recents
|
? recents.filter(
|
||||||
.filter(
|
(item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID),
|
||||||
(item) => !favorites.some((fav) => fav.providerID === item.providerID && fav.modelID === item.modelID),
|
)
|
||||||
)
|
|
||||||
.slice(0, 5)
|
|
||||||
: []
|
: []
|
||||||
|
|
||||||
const favoriteOptions = favorites.flatMap((item) => {
|
const favoriteOptions = favorites.flatMap((item) => {
|
||||||
@@ -182,7 +180,10 @@ export function DialogModel(props: { providerID?: string }) {
|
|||||||
// Apply fuzzy filtering to each section separately, maintaining section order
|
// Apply fuzzy filtering to each section separately, maintaining section order
|
||||||
if (q) {
|
if (q) {
|
||||||
const filteredFavorites = fuzzysort.go(q, favoriteOptions, { keys: ["title"] }).map((x) => x.obj)
|
const filteredFavorites = fuzzysort.go(q, favoriteOptions, { keys: ["title"] }).map((x) => x.obj)
|
||||||
const filteredRecents = fuzzysort.go(q, recentOptions, { keys: ["title"] }).map((x) => x.obj)
|
const filteredRecents = fuzzysort
|
||||||
|
.go(q, recentOptions, { keys: ["title"] })
|
||||||
|
.map((x) => x.obj)
|
||||||
|
.slice(0, 5)
|
||||||
const filteredProviders = fuzzysort.go(q, providerOptions, { keys: ["title", "category"] }).map((x) => x.obj)
|
const filteredProviders = fuzzysort.go(q, providerOptions, { keys: ["title", "category"] }).map((x) => x.obj)
|
||||||
const filteredPopular = fuzzysort.go(q, popularProviders, { keys: ["title"] }).map((x) => x.obj)
|
const filteredPopular = fuzzysort.go(q, popularProviders, { keys: ["title"] }).map((x) => x.obj)
|
||||||
return [...filteredFavorites, ...filteredRecents, ...filteredProviders, ...filteredPopular]
|
return [...filteredFavorites, ...filteredRecents, ...filteredProviders, ...filteredPopular]
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { DialogSelect } from "@tui/ui/dialog-select"
|
|||||||
import { useDialog } from "@tui/ui/dialog"
|
import { useDialog } from "@tui/ui/dialog"
|
||||||
import { useSDK } from "../context/sdk"
|
import { useSDK } from "../context/sdk"
|
||||||
import { DialogPrompt } from "../ui/dialog-prompt"
|
import { DialogPrompt } from "../ui/dialog-prompt"
|
||||||
|
import { Link } from "../ui/link"
|
||||||
import { useTheme } from "../context/theme"
|
import { useTheme } from "../context/theme"
|
||||||
import { TextAttributes } from "@opentui/core"
|
import { TextAttributes } from "@opentui/core"
|
||||||
import type { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2"
|
import type { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2"
|
||||||
@@ -128,7 +129,7 @@ function AutoMethod(props: AutoMethodProps) {
|
|||||||
<text fg={theme.textMuted}>esc</text>
|
<text fg={theme.textMuted}>esc</text>
|
||||||
</box>
|
</box>
|
||||||
<box gap={1}>
|
<box gap={1}>
|
||||||
<text fg={theme.primary}>{props.authorization.url}</text>
|
<Link href={props.authorization.url} fg={theme.primary} />
|
||||||
<text fg={theme.textMuted}>{props.authorization.instructions}</text>
|
<text fg={theme.textMuted}>{props.authorization.instructions}</text>
|
||||||
</box>
|
</box>
|
||||||
<text fg={theme.textMuted}>Waiting for authorization...</text>
|
<text fg={theme.textMuted}>Waiting for authorization...</text>
|
||||||
@@ -170,7 +171,7 @@ function CodeMethod(props: CodeMethodProps) {
|
|||||||
description={() => (
|
description={() => (
|
||||||
<box gap={1}>
|
<box gap={1}>
|
||||||
<text fg={theme.textMuted}>{props.authorization.instructions}</text>
|
<text fg={theme.textMuted}>{props.authorization.instructions}</text>
|
||||||
<text fg={theme.primary}>{props.authorization.url}</text>
|
<Link href={props.authorization.url} fg={theme.primary} />
|
||||||
<Show when={error()}>
|
<Show when={error()}>
|
||||||
<text fg={theme.error}>Invalid code</text>
|
<text fg={theme.error}>Invalid code</text>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import { useDialog } from "@tui/ui/dialog"
|
|||||||
import { DialogSelect } from "@tui/ui/dialog-select"
|
import { DialogSelect } from "@tui/ui/dialog-select"
|
||||||
import { useRoute } from "@tui/context/route"
|
import { useRoute } from "@tui/context/route"
|
||||||
import { useSync } from "@tui/context/sync"
|
import { useSync } from "@tui/context/sync"
|
||||||
import { createEffect, createMemo, createSignal, onMount } from "solid-js"
|
import { createEffect, createMemo, createSignal, onMount, Show } from "solid-js"
|
||||||
import { Locale } from "@/util/locale"
|
import { Locale } from "@/util/locale"
|
||||||
import { Keybind } from "@/util/keybind"
|
import { Keybind } from "@/util/keybind"
|
||||||
import { useTheme } from "../context/theme"
|
import { useTheme } from "../context/theme"
|
||||||
import { useSDK } from "../context/sdk"
|
import { useSDK } from "../context/sdk"
|
||||||
import { DialogSessionRename } from "./dialog-session-rename"
|
import { DialogSessionRename } from "./dialog-session-rename"
|
||||||
|
import { useKV } from "../context/kv"
|
||||||
import "opentui-spinner/solid"
|
import "opentui-spinner/solid"
|
||||||
|
|
||||||
export function DialogSessionList() {
|
export function DialogSessionList() {
|
||||||
@@ -16,6 +17,7 @@ export function DialogSessionList() {
|
|||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
const kv = useKV()
|
||||||
|
|
||||||
const [toDelete, setToDelete] = createSignal<string>()
|
const [toDelete, setToDelete] = createSignal<string>()
|
||||||
|
|
||||||
@@ -45,7 +47,11 @@ export function DialogSessionList() {
|
|||||||
value: x.id,
|
value: x.id,
|
||||||
category,
|
category,
|
||||||
footer: Locale.time(x.time.updated),
|
footer: Locale.time(x.time.updated),
|
||||||
gutter: isWorking ? <spinner frames={spinnerFrames} interval={80} color={theme.primary} /> : undefined,
|
gutter: isWorking ? (
|
||||||
|
<Show when={kv.get("animations_enabled", true)} fallback={<text fg={theme.textMuted}>[⋯]</text>}>
|
||||||
|
<spinner frames={spinnerFrames} interval={80} color={theme.primary} />
|
||||||
|
</Show>
|
||||||
|
) : undefined,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.slice(0, 150)
|
.slice(0, 150)
|
||||||
|
|||||||
@@ -12,6 +12,38 @@ import { useTerminalDimensions } from "@opentui/solid"
|
|||||||
import { Locale } from "@/util/locale"
|
import { Locale } from "@/util/locale"
|
||||||
import type { PromptInfo } from "./history"
|
import type { PromptInfo } from "./history"
|
||||||
|
|
||||||
|
function removeLineRange(input: string) {
|
||||||
|
const hashIndex = input.lastIndexOf("#")
|
||||||
|
return hashIndex !== -1 ? input.substring(0, hashIndex) : input
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractLineRange(input: string) {
|
||||||
|
const hashIndex = input.lastIndexOf("#")
|
||||||
|
if (hashIndex === -1) {
|
||||||
|
return { baseQuery: input }
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseName = input.substring(0, hashIndex)
|
||||||
|
const linePart = input.substring(hashIndex + 1)
|
||||||
|
const lineMatch = linePart.match(/^(\d+)(?:-(\d*))?$/)
|
||||||
|
|
||||||
|
if (!lineMatch) {
|
||||||
|
return { baseQuery: baseName }
|
||||||
|
}
|
||||||
|
|
||||||
|
const startLine = Number(lineMatch[1])
|
||||||
|
const endLine = lineMatch[2] && startLine < Number(lineMatch[2]) ? Number(lineMatch[2]) : undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
lineRange: {
|
||||||
|
baseName,
|
||||||
|
startLine,
|
||||||
|
endLine,
|
||||||
|
},
|
||||||
|
baseQuery: baseName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type AutocompleteRef = {
|
export type AutocompleteRef = {
|
||||||
onInput: (value: string) => void
|
onInput: (value: string) => void
|
||||||
onKeyDown: (e: KeyEvent) => void
|
onKeyDown: (e: KeyEvent) => void
|
||||||
@@ -142,9 +174,11 @@ export function Autocomplete(props: {
|
|||||||
async (query) => {
|
async (query) => {
|
||||||
if (!store.visible || store.visible === "/") return []
|
if (!store.visible || store.visible === "/") return []
|
||||||
|
|
||||||
|
const { lineRange, baseQuery } = extractLineRange(query ?? "")
|
||||||
|
|
||||||
// Get files from SDK
|
// Get files from SDK
|
||||||
const result = await sdk.client.find.files({
|
const result = await sdk.client.find.files({
|
||||||
query: query ?? "",
|
query: baseQuery,
|
||||||
})
|
})
|
||||||
|
|
||||||
const options: AutocompleteOption[] = []
|
const options: AutocompleteOption[] = []
|
||||||
@@ -153,15 +187,27 @@ export function Autocomplete(props: {
|
|||||||
if (!result.error && result.data) {
|
if (!result.error && result.data) {
|
||||||
const width = props.anchor().width - 4
|
const width = props.anchor().width - 4
|
||||||
options.push(
|
options.push(
|
||||||
...result.data.map(
|
...result.data.map((item): AutocompleteOption => {
|
||||||
(item): AutocompleteOption => ({
|
let url = `file://${process.cwd()}/${item}`
|
||||||
display: Locale.truncateMiddle(item, width),
|
let filename = item
|
||||||
|
if (lineRange && !item.endsWith("/")) {
|
||||||
|
filename = `${item}#${lineRange.startLine}${lineRange.endLine ? `-${lineRange.endLine}` : ""}`
|
||||||
|
const urlObj = new URL(url)
|
||||||
|
urlObj.searchParams.set("start", String(lineRange.startLine))
|
||||||
|
if (lineRange.endLine !== undefined) {
|
||||||
|
urlObj.searchParams.set("end", String(lineRange.endLine))
|
||||||
|
}
|
||||||
|
url = urlObj.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
display: Locale.truncateMiddle(filename, width),
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
insertPart(item, {
|
insertPart(filename, {
|
||||||
type: "file",
|
type: "file",
|
||||||
mime: "text/plain",
|
mime: "text/plain",
|
||||||
filename: item,
|
filename,
|
||||||
url: `file://${process.cwd()}/${item}`,
|
url,
|
||||||
source: {
|
source: {
|
||||||
type: "file",
|
type: "file",
|
||||||
text: {
|
text: {
|
||||||
@@ -173,8 +219,8 @@ export function Autocomplete(props: {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}),
|
}
|
||||||
),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,8 +429,8 @@ export function Autocomplete(props: {
|
|||||||
return prev
|
return prev
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = fuzzysort.go(currentFilter, mixed, {
|
const result = fuzzysort.go(removeLineRange(currentFilter), mixed, {
|
||||||
keys: [(obj) => obj.display.trimEnd(), "description", (obj) => obj.aliases?.join(" ") ?? ""],
|
keys: [(obj) => removeLineRange(obj.display.trimEnd()), "description", (obj) => obj.aliases?.join(" ") ?? ""],
|
||||||
limit: 10,
|
limit: 10,
|
||||||
scoreFn: (objResults) => {
|
scoreFn: (objResults) => {
|
||||||
const displayResult = objResults[0]
|
const displayResult = objResults[0]
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { useDialog } from "@tui/ui/dialog"
|
|||||||
import { DialogProvider as DialogProviderConnect } from "../dialog-provider"
|
import { DialogProvider as DialogProviderConnect } from "../dialog-provider"
|
||||||
import { DialogAlert } from "../../ui/dialog-alert"
|
import { DialogAlert } from "../../ui/dialog-alert"
|
||||||
import { useToast } from "../../ui/toast"
|
import { useToast } from "../../ui/toast"
|
||||||
|
import { useKV } from "../../context/kv"
|
||||||
|
|
||||||
export type PromptProps = {
|
export type PromptProps = {
|
||||||
sessionID?: string
|
sessionID?: string
|
||||||
@@ -124,6 +125,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
const command = useCommandDialog()
|
const command = useCommandDialog()
|
||||||
const renderer = useRenderer()
|
const renderer = useRenderer()
|
||||||
const { theme, syntax } = useTheme()
|
const { theme, syntax } = useTheme()
|
||||||
|
const kv = useKV()
|
||||||
|
|
||||||
function promptModelWarning() {
|
function promptModelWarning() {
|
||||||
toast.show({
|
toast.show({
|
||||||
@@ -996,8 +998,11 @@ export function Prompt(props: PromptProps) {
|
|||||||
justifyContent={status().type === "retry" ? "space-between" : "flex-start"}
|
justifyContent={status().type === "retry" ? "space-between" : "flex-start"}
|
||||||
>
|
>
|
||||||
<box flexShrink={0} flexDirection="row" gap={1}>
|
<box flexShrink={0} flexDirection="row" gap={1}>
|
||||||
{/* @ts-ignore // SpinnerOptions doesn't support marginLeft */}
|
<box marginLeft={1}>
|
||||||
<spinner marginLeft={1} color={spinnerDef().color} frames={spinnerDef().frames} interval={40} />
|
<Show when={kv.get("animations_enabled", true)} fallback={<text fg={theme.textMuted}>[⋯]</text>}>
|
||||||
|
<spinner color={spinnerDef().color} frames={spinnerDef().frames} interval={40} />
|
||||||
|
</Show>
|
||||||
|
</box>
|
||||||
<box flexDirection="row" gap={1} flexShrink={0}>
|
<box flexDirection="row" gap={1} flexShrink={0}>
|
||||||
{(() => {
|
{(() => {
|
||||||
const retry = createMemo(() => {
|
const retry = createMemo(() => {
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ import { DialogMessage } from "./dialog-message"
|
|||||||
import type { PromptInfo } from "../../component/prompt/history"
|
import type { PromptInfo } from "../../component/prompt/history"
|
||||||
import { iife } from "@/util/iife"
|
import { iife } from "@/util/iife"
|
||||||
import { DialogConfirm } from "@tui/ui/dialog-confirm"
|
import { DialogConfirm } from "@tui/ui/dialog-confirm"
|
||||||
import { DialogPrompt } from "@tui/ui/dialog-prompt"
|
|
||||||
import { DialogTimeline } from "./dialog-timeline"
|
import { DialogTimeline } from "./dialog-timeline"
|
||||||
import { DialogForkFromTimeline } from "./dialog-fork-from-timeline"
|
import { DialogForkFromTimeline } from "./dialog-fork-from-timeline"
|
||||||
import { DialogSessionRename } from "../../component/dialog-session-rename"
|
import { DialogSessionRename } from "../../component/dialog-session-rename"
|
||||||
@@ -67,7 +66,7 @@ import stripAnsi from "strip-ansi"
|
|||||||
import { Footer } from "./footer.tsx"
|
import { Footer } from "./footer.tsx"
|
||||||
import { usePromptRef } from "../../context/prompt"
|
import { usePromptRef } from "../../context/prompt"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
import { DialogSubagent } from "./dialog-subagent.tsx"
|
import { DialogExportOptions } from "../../ui/dialog-export-options"
|
||||||
|
|
||||||
addDefaultParsers(parsers.parsers)
|
addDefaultParsers(parsers.parsers)
|
||||||
|
|
||||||
@@ -128,6 +127,7 @@ export function Session() {
|
|||||||
const [showScrollbar, setShowScrollbar] = createSignal(kv.get("scrollbar_visible", false))
|
const [showScrollbar, setShowScrollbar] = createSignal(kv.get("scrollbar_visible", false))
|
||||||
const [userMessageMarkdown, setUserMessageMarkdown] = createSignal(kv.get("user_message_markdown", true))
|
const [userMessageMarkdown, setUserMessageMarkdown] = createSignal(kv.get("user_message_markdown", true))
|
||||||
const [diffWrapMode, setDiffWrapMode] = createSignal<"word" | "none">("word")
|
const [diffWrapMode, setDiffWrapMode] = createSignal<"word" | "none">("word")
|
||||||
|
const [animationsEnabled, setAnimationsEnabled] = createSignal(kv.get("animations_enabled", true))
|
||||||
|
|
||||||
const wide = createMemo(() => dimensions().width > 120)
|
const wide = createMemo(() => dimensions().width > 120)
|
||||||
const sidebarVisible = createMemo(() => {
|
const sidebarVisible = createMemo(() => {
|
||||||
@@ -584,6 +584,19 @@ export function Session() {
|
|||||||
dialog.clear()
|
dialog.clear()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: animationsEnabled() ? "Disable animations" : "Enable animations",
|
||||||
|
value: "session.toggle.animations",
|
||||||
|
category: "Session",
|
||||||
|
onSelect: (dialog) => {
|
||||||
|
setAnimationsEnabled((prev) => {
|
||||||
|
const next = !prev
|
||||||
|
kv.set("animations_enabled", next)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
dialog.clear()
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Page up",
|
title: "Page up",
|
||||||
value: "session.page.up",
|
value: "session.page.up",
|
||||||
@@ -770,8 +783,22 @@ export function Session() {
|
|||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
if (part.type === "text" && !part.synthetic) {
|
if (part.type === "text" && !part.synthetic) {
|
||||||
transcript += `${part.text}\n\n`
|
transcript += `${part.text}\n\n`
|
||||||
|
} else if (part.type === "reasoning") {
|
||||||
|
if (showThinking()) {
|
||||||
|
transcript += `_Thinking:_\n\n${part.text}\n\n`
|
||||||
|
}
|
||||||
} else if (part.type === "tool") {
|
} else if (part.type === "tool") {
|
||||||
transcript += `\`\`\`\nTool: ${part.tool}\n\`\`\`\n\n`
|
transcript += `\`\`\`\nTool: ${part.tool}\n`
|
||||||
|
if (showDetails() && part.state.input) {
|
||||||
|
transcript += `\n**Input:**\n\`\`\`json\n${JSON.stringify(part.state.input, null, 2)}\n\`\`\``
|
||||||
|
}
|
||||||
|
if (showDetails() && part.state.status === "completed" && part.state.output) {
|
||||||
|
transcript += `\n**Output:**\n\`\`\`\n${part.state.output}\n\`\`\``
|
||||||
|
}
|
||||||
|
if (showDetails() && part.state.status === "error" && part.state.error) {
|
||||||
|
transcript += `\n**Error:**\n\`\`\`\n${part.state.error}\n\`\`\``
|
||||||
|
}
|
||||||
|
transcript += `\n\`\`\`\n\n`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -798,6 +825,14 @@ export function Session() {
|
|||||||
const sessionData = session()
|
const sessionData = session()
|
||||||
const sessionMessages = messages()
|
const sessionMessages = messages()
|
||||||
|
|
||||||
|
const defaultFilename = `session-${sessionData.id.slice(0, 8)}.md`
|
||||||
|
|
||||||
|
const options = await DialogExportOptions.show(dialog, defaultFilename, showThinking(), showDetails())
|
||||||
|
|
||||||
|
if (options === null) return
|
||||||
|
|
||||||
|
const { filename: customFilename, thinking: includeThinking, toolDetails: includeToolDetails } = options
|
||||||
|
|
||||||
let transcript = `# ${sessionData.title}\n\n`
|
let transcript = `# ${sessionData.title}\n\n`
|
||||||
transcript += `**Session ID:** ${sessionData.id}\n`
|
transcript += `**Session ID:** ${sessionData.id}\n`
|
||||||
transcript += `**Created:** ${new Date(sessionData.time.created).toLocaleString()}\n`
|
transcript += `**Created:** ${new Date(sessionData.time.created).toLocaleString()}\n`
|
||||||
@@ -812,22 +847,28 @@ export function Session() {
|
|||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
if (part.type === "text" && !part.synthetic) {
|
if (part.type === "text" && !part.synthetic) {
|
||||||
transcript += `${part.text}\n\n`
|
transcript += `${part.text}\n\n`
|
||||||
|
} else if (part.type === "reasoning") {
|
||||||
|
if (includeThinking) {
|
||||||
|
transcript += `_Thinking:_\n\n${part.text}\n\n`
|
||||||
|
}
|
||||||
} else if (part.type === "tool") {
|
} else if (part.type === "tool") {
|
||||||
transcript += `\`\`\`\nTool: ${part.tool}\n\`\`\`\n\n`
|
transcript += `\`\`\`\nTool: ${part.tool}\n`
|
||||||
|
if (includeToolDetails && part.state.input) {
|
||||||
|
transcript += `\n**Input:**\n\`\`\`json\n${JSON.stringify(part.state.input, null, 2)}\n\`\`\``
|
||||||
|
}
|
||||||
|
if (includeToolDetails && part.state.status === "completed" && part.state.output) {
|
||||||
|
transcript += `\n**Output:**\n\`\`\`\n${part.state.output}\n\`\`\``
|
||||||
|
}
|
||||||
|
if (includeToolDetails && part.state.status === "error" && part.state.error) {
|
||||||
|
transcript += `\n**Error:**\n\`\`\`\n${part.state.error}\n\`\`\``
|
||||||
|
}
|
||||||
|
transcript += `\n\`\`\`\n\n`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
transcript += `---\n\n`
|
transcript += `---\n\n`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prompt for optional filename
|
|
||||||
const customFilename = await DialogPrompt.show(dialog, "Export filename", {
|
|
||||||
value: `session-${sessionData.id.slice(0, 8)}.md`,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Cancel if user pressed escape
|
|
||||||
if (customFilename === null) return
|
|
||||||
|
|
||||||
// Save to file in current working directory
|
// Save to file in current working directory
|
||||||
const exportDir = process.cwd()
|
const exportDir = process.cwd()
|
||||||
const filename = customFilename.trim()
|
const filename = customFilename.trim()
|
||||||
|
|||||||
@@ -3,31 +3,19 @@ import { Instance } from "@/project/instance"
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { Server } from "@/server/server"
|
import { Server } from "@/server/server"
|
||||||
import { upgrade } from "@/cli/upgrade"
|
import { upgrade } from "@/cli/upgrade"
|
||||||
|
import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
|
||||||
|
|
||||||
export const TuiSpawnCommand = cmd({
|
export const TuiSpawnCommand = cmd({
|
||||||
command: "spawn [project]",
|
command: "spawn [project]",
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
yargs
|
withNetworkOptions(yargs).positional("project", {
|
||||||
.positional("project", {
|
type: "string",
|
||||||
type: "string",
|
describe: "path to start opencode in",
|
||||||
describe: "path to start opencode in",
|
}),
|
||||||
})
|
|
||||||
.option("port", {
|
|
||||||
type: "number",
|
|
||||||
describe: "port to listen on",
|
|
||||||
default: 0,
|
|
||||||
})
|
|
||||||
.option("hostname", {
|
|
||||||
type: "string",
|
|
||||||
describe: "hostname to listen on",
|
|
||||||
default: "127.0.0.1",
|
|
||||||
}),
|
|
||||||
handler: async (args) => {
|
handler: async (args) => {
|
||||||
upgrade()
|
upgrade()
|
||||||
const server = Server.listen({
|
const opts = await resolveNetworkOptions(args)
|
||||||
port: args.port,
|
const server = Server.listen(opts)
|
||||||
hostname: "127.0.0.1",
|
|
||||||
})
|
|
||||||
const bin = process.execPath
|
const bin = process.execPath
|
||||||
const cmd = []
|
const cmd = []
|
||||||
let cwd = process.cwd()
|
let cwd = process.cwd()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import path from "path"
|
|||||||
import { UI } from "@/cli/ui"
|
import { UI } from "@/cli/ui"
|
||||||
import { iife } from "@/util/iife"
|
import { iife } from "@/util/iife"
|
||||||
import { Log } from "@/util/log"
|
import { Log } from "@/util/log"
|
||||||
|
import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
const OPENCODE_WORKER_PATH: string
|
const OPENCODE_WORKER_PATH: string
|
||||||
@@ -15,7 +16,7 @@ export const TuiThreadCommand = cmd({
|
|||||||
command: "$0 [project]",
|
command: "$0 [project]",
|
||||||
describe: "start opencode tui",
|
describe: "start opencode tui",
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
yargs
|
withNetworkOptions(yargs)
|
||||||
.positional("project", {
|
.positional("project", {
|
||||||
type: "string",
|
type: "string",
|
||||||
describe: "path to start opencode in",
|
describe: "path to start opencode in",
|
||||||
@@ -36,23 +37,12 @@ export const TuiThreadCommand = cmd({
|
|||||||
describe: "session id to continue",
|
describe: "session id to continue",
|
||||||
})
|
})
|
||||||
.option("prompt", {
|
.option("prompt", {
|
||||||
alias: ["p"],
|
|
||||||
type: "string",
|
type: "string",
|
||||||
describe: "prompt to use",
|
describe: "prompt to use",
|
||||||
})
|
})
|
||||||
.option("agent", {
|
.option("agent", {
|
||||||
type: "string",
|
type: "string",
|
||||||
describe: "agent to use",
|
describe: "agent to use",
|
||||||
})
|
|
||||||
.option("port", {
|
|
||||||
type: "number",
|
|
||||||
describe: "port to listen on",
|
|
||||||
default: 0,
|
|
||||||
})
|
|
||||||
.option("hostname", {
|
|
||||||
type: "string",
|
|
||||||
describe: "hostname to listen on",
|
|
||||||
default: "127.0.0.1",
|
|
||||||
}),
|
}),
|
||||||
handler: async (args) => {
|
handler: async (args) => {
|
||||||
// Resolve relative paths against PWD to preserve behavior when using --cwd flag
|
// Resolve relative paths against PWD to preserve behavior when using --cwd flag
|
||||||
@@ -87,10 +77,8 @@ export const TuiThreadCommand = cmd({
|
|||||||
process.on("unhandledRejection", (e) => {
|
process.on("unhandledRejection", (e) => {
|
||||||
Log.Default.error(e)
|
Log.Default.error(e)
|
||||||
})
|
})
|
||||||
const server = await client.call("server", {
|
const opts = await resolveNetworkOptions(args)
|
||||||
port: args.port,
|
const server = await client.call("server", opts)
|
||||||
hostname: args.hostname,
|
|
||||||
})
|
|
||||||
const prompt = await iife(async () => {
|
const prompt = await iife(async () => {
|
||||||
const piped = !process.stdin.isTTY ? await Bun.stdin.text() : undefined
|
const piped = !process.stdin.isTTY ? await Bun.stdin.text() : undefined
|
||||||
if (!args.prompt) return piped
|
if (!args.prompt) return piped
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import { TextareaRenderable, TextAttributes } from "@opentui/core"
|
||||||
|
import { useTheme } from "../context/theme"
|
||||||
|
import { useDialog, type DialogContext } from "./dialog"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { onMount, Show, type JSX } from "solid-js"
|
||||||
|
import { useKeyboard } from "@opentui/solid"
|
||||||
|
|
||||||
|
export type DialogExportOptionsProps = {
|
||||||
|
defaultFilename: string
|
||||||
|
defaultThinking: boolean
|
||||||
|
defaultToolDetails: boolean
|
||||||
|
onConfirm?: (options: { filename: string; thinking: boolean; toolDetails: boolean }) => void
|
||||||
|
onCancel?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DialogExportOptions(props: DialogExportOptionsProps) {
|
||||||
|
const dialog = useDialog()
|
||||||
|
const { theme } = useTheme()
|
||||||
|
let textarea: TextareaRenderable
|
||||||
|
const [store, setStore] = createStore({
|
||||||
|
thinking: props.defaultThinking,
|
||||||
|
toolDetails: props.defaultToolDetails,
|
||||||
|
active: "filename" as "filename" | "thinking" | "toolDetails",
|
||||||
|
})
|
||||||
|
|
||||||
|
useKeyboard((evt) => {
|
||||||
|
if (evt.name === "return") {
|
||||||
|
props.onConfirm?.({
|
||||||
|
filename: textarea.plainText,
|
||||||
|
thinking: store.thinking,
|
||||||
|
toolDetails: store.toolDetails,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (evt.name === "tab") {
|
||||||
|
const order: Array<"filename" | "thinking" | "toolDetails"> = ["filename", "thinking", "toolDetails"]
|
||||||
|
const currentIndex = order.indexOf(store.active)
|
||||||
|
const nextIndex = (currentIndex + 1) % order.length
|
||||||
|
setStore("active", order[nextIndex])
|
||||||
|
evt.preventDefault()
|
||||||
|
}
|
||||||
|
if (evt.name === "space") {
|
||||||
|
if (store.active === "thinking") setStore("thinking", !store.thinking)
|
||||||
|
if (store.active === "toolDetails") setStore("toolDetails", !store.toolDetails)
|
||||||
|
evt.preventDefault()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
dialog.setSize("medium")
|
||||||
|
setTimeout(() => {
|
||||||
|
textarea.focus()
|
||||||
|
}, 1)
|
||||||
|
textarea.gotoLineEnd()
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box paddingLeft={2} paddingRight={2} gap={1}>
|
||||||
|
<box flexDirection="row" justifyContent="space-between">
|
||||||
|
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
||||||
|
Export Options
|
||||||
|
</text>
|
||||||
|
<text fg={theme.textMuted}>esc</text>
|
||||||
|
</box>
|
||||||
|
<box gap={1}>
|
||||||
|
<box>
|
||||||
|
<text fg={theme.text}>Filename:</text>
|
||||||
|
</box>
|
||||||
|
<textarea
|
||||||
|
onSubmit={() => {
|
||||||
|
props.onConfirm?.({
|
||||||
|
filename: textarea.plainText,
|
||||||
|
thinking: store.thinking,
|
||||||
|
toolDetails: store.toolDetails,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
height={3}
|
||||||
|
keyBindings={[{ name: "return", action: "submit" }]}
|
||||||
|
ref={(val: TextareaRenderable) => (textarea = val)}
|
||||||
|
initialValue={props.defaultFilename}
|
||||||
|
placeholder="Enter filename"
|
||||||
|
textColor={theme.text}
|
||||||
|
focusedTextColor={theme.text}
|
||||||
|
cursorColor={theme.text}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
<box flexDirection="column">
|
||||||
|
<box
|
||||||
|
flexDirection="row"
|
||||||
|
gap={2}
|
||||||
|
paddingLeft={1}
|
||||||
|
backgroundColor={store.active === "thinking" ? theme.backgroundElement : undefined}
|
||||||
|
onMouseUp={() => setStore("active", "thinking")}
|
||||||
|
>
|
||||||
|
<text fg={store.active === "thinking" ? theme.primary : theme.textMuted}>
|
||||||
|
{store.thinking ? "[x]" : "[ ]"}
|
||||||
|
</text>
|
||||||
|
<text fg={store.active === "thinking" ? theme.primary : theme.text}>Include thinking</text>
|
||||||
|
</box>
|
||||||
|
<box
|
||||||
|
flexDirection="row"
|
||||||
|
gap={2}
|
||||||
|
paddingLeft={1}
|
||||||
|
backgroundColor={store.active === "toolDetails" ? theme.backgroundElement : undefined}
|
||||||
|
onMouseUp={() => setStore("active", "toolDetails")}
|
||||||
|
>
|
||||||
|
<text fg={store.active === "toolDetails" ? theme.primary : theme.textMuted}>
|
||||||
|
{store.toolDetails ? "[x]" : "[ ]"}
|
||||||
|
</text>
|
||||||
|
<text fg={store.active === "toolDetails" ? theme.primary : theme.text}>Include tool details</text>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
<Show when={store.active !== "filename"}>
|
||||||
|
<text fg={theme.textMuted} paddingBottom={1}>
|
||||||
|
Press <span style={{ fg: theme.text }}>space</span> to toggle, <span style={{ fg: theme.text }}>return</span>{" "}
|
||||||
|
to confirm
|
||||||
|
</text>
|
||||||
|
</Show>
|
||||||
|
<Show when={store.active === "filename"}>
|
||||||
|
<text fg={theme.textMuted} paddingBottom={1}>
|
||||||
|
Press <span style={{ fg: theme.text }}>return</span> to confirm, <span style={{ fg: theme.text }}>tab</span>{" "}
|
||||||
|
for options
|
||||||
|
</text>
|
||||||
|
</Show>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogExportOptions.show = (
|
||||||
|
dialog: DialogContext,
|
||||||
|
defaultFilename: string,
|
||||||
|
defaultThinking: boolean,
|
||||||
|
defaultToolDetails: boolean,
|
||||||
|
) => {
|
||||||
|
return new Promise<{ filename: string; thinking: boolean; toolDetails: boolean } | null>((resolve) => {
|
||||||
|
dialog.replace(
|
||||||
|
() => (
|
||||||
|
<DialogExportOptions
|
||||||
|
defaultFilename={defaultFilename}
|
||||||
|
defaultThinking={defaultThinking}
|
||||||
|
defaultToolDetails={defaultToolDetails}
|
||||||
|
onConfirm={(options) => resolve(options)}
|
||||||
|
onCancel={() => resolve(null)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
() => resolve(null),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { JSX } from "solid-js"
|
||||||
|
import type { RGBA } from "@opentui/core"
|
||||||
|
import open from "open"
|
||||||
|
|
||||||
|
export interface LinkProps {
|
||||||
|
href: string
|
||||||
|
children?: JSX.Element | string
|
||||||
|
fg?: RGBA
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Link component that renders clickable hyperlinks.
|
||||||
|
* Clicking anywhere on the link text opens the URL in the default browser.
|
||||||
|
*/
|
||||||
|
export function Link(props: LinkProps) {
|
||||||
|
const displayText = props.children ?? props.href
|
||||||
|
|
||||||
|
return (
|
||||||
|
<text
|
||||||
|
fg={props.fg}
|
||||||
|
onMouseUp={() => {
|
||||||
|
open(props.href).catch(() => {})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{displayText}
|
||||||
|
</text>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ process.on("uncaughtException", (e) => {
|
|||||||
|
|
||||||
let server: Bun.Server<BunWebSocketData>
|
let server: Bun.Server<BunWebSocketData>
|
||||||
export const rpc = {
|
export const rpc = {
|
||||||
async server(input: { port: number; hostname: string }) {
|
async server(input: { port: number; hostname: string; mdns?: boolean }) {
|
||||||
if (server) await server.stop(true)
|
if (server) await server.stop(true)
|
||||||
try {
|
try {
|
||||||
server = Server.listen(input)
|
server = Server.listen(input)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Server } from "../../server/server"
|
import { Server } from "../../server/server"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
|
import { withNetworkOptions, resolveNetworkOptions } from "../network"
|
||||||
import open from "open"
|
import open from "open"
|
||||||
import { networkInterfaces } from "os"
|
import { networkInterfaces } from "os"
|
||||||
|
|
||||||
@@ -28,32 +29,16 @@ function getNetworkIPs() {
|
|||||||
|
|
||||||
export const WebCommand = cmd({
|
export const WebCommand = cmd({
|
||||||
command: "web",
|
command: "web",
|
||||||
builder: (yargs) =>
|
builder: (yargs) => withNetworkOptions(yargs),
|
||||||
yargs
|
|
||||||
.option("port", {
|
|
||||||
alias: ["p"],
|
|
||||||
type: "number",
|
|
||||||
describe: "port to listen on",
|
|
||||||
default: 0,
|
|
||||||
})
|
|
||||||
.option("hostname", {
|
|
||||||
type: "string",
|
|
||||||
describe: "hostname to listen on",
|
|
||||||
default: "127.0.0.1",
|
|
||||||
}),
|
|
||||||
describe: "starts a headless opencode server",
|
describe: "starts a headless opencode server",
|
||||||
handler: async (args) => {
|
handler: async (args) => {
|
||||||
const hostname = args.hostname
|
const opts = await resolveNetworkOptions(args)
|
||||||
const port = args.port
|
const server = Server.listen(opts)
|
||||||
const server = Server.listen({
|
|
||||||
port,
|
|
||||||
hostname,
|
|
||||||
})
|
|
||||||
UI.empty()
|
UI.empty()
|
||||||
UI.println(UI.logo(" "))
|
UI.println(UI.logo(" "))
|
||||||
UI.empty()
|
UI.empty()
|
||||||
|
|
||||||
if (hostname === "0.0.0.0") {
|
if (opts.hostname === "0.0.0.0") {
|
||||||
// Show localhost for local access
|
// Show localhost for local access
|
||||||
const localhostUrl = `http://localhost:${server.port}`
|
const localhostUrl = `http://localhost:${server.port}`
|
||||||
UI.println(UI.Style.TEXT_INFO_BOLD + " Local access: ", UI.Style.TEXT_NORMAL, localhostUrl)
|
UI.println(UI.Style.TEXT_INFO_BOLD + " Local access: ", UI.Style.TEXT_NORMAL, localhostUrl)
|
||||||
@@ -70,6 +55,10 @@ export const WebCommand = cmd({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts.mdns) {
|
||||||
|
UI.println(UI.Style.TEXT_INFO_BOLD + " mDNS: ", UI.Style.TEXT_NORMAL, "opencode.local")
|
||||||
|
}
|
||||||
|
|
||||||
// Open localhost in browser
|
// Open localhost in browser
|
||||||
open(localhostUrl.toString()).catch(() => {})
|
open(localhostUrl.toString()).catch(() => {})
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import type { Argv, InferredOptionTypes } from "yargs"
|
||||||
|
import { Config } from "../config/config"
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
port: {
|
||||||
|
type: "number" as const,
|
||||||
|
describe: "port to listen on",
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
hostname: {
|
||||||
|
type: "string" as const,
|
||||||
|
describe: "hostname to listen on",
|
||||||
|
default: "127.0.0.1",
|
||||||
|
},
|
||||||
|
mdns: {
|
||||||
|
type: "boolean" as const,
|
||||||
|
describe: "enable mDNS service discovery (defaults hostname to 0.0.0.0)",
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NetworkOptions = InferredOptionTypes<typeof options>
|
||||||
|
|
||||||
|
export function withNetworkOptions<T>(yargs: Argv<T>) {
|
||||||
|
return yargs.options(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveNetworkOptions(args: NetworkOptions) {
|
||||||
|
const config = await Config.global()
|
||||||
|
const portExplicitlySet = process.argv.includes("--port")
|
||||||
|
const hostnameExplicitlySet = process.argv.includes("--hostname")
|
||||||
|
const mdnsExplicitlySet = process.argv.includes("--mdns")
|
||||||
|
|
||||||
|
const mdns = mdnsExplicitlySet ? args.mdns : (config?.server?.mdns ?? args.mdns)
|
||||||
|
const port = portExplicitlySet ? args.port : (config?.server?.port ?? args.port)
|
||||||
|
const hostname = hostnameExplicitlySet
|
||||||
|
? args.hostname
|
||||||
|
: mdns && !config?.server?.hostname
|
||||||
|
? "0.0.0.0"
|
||||||
|
: (config?.server?.hostname ?? args.hostname)
|
||||||
|
|
||||||
|
return { hostname, port, mdns }
|
||||||
|
}
|
||||||
@@ -98,7 +98,7 @@ export namespace Config {
|
|||||||
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
||||||
log.debug(`loading config from ${path.join(dir, file)}`)
|
log.debug(`loading config from ${path.join(dir, file)}`)
|
||||||
result = mergeConfigWithPlugins(result, await loadFile(path.join(dir, file)))
|
result = mergeConfigWithPlugins(result, await loadFile(path.join(dir, file)))
|
||||||
// to satisy the type checker
|
// to satisfy the type checker
|
||||||
result.agent ??= {}
|
result.agent ??= {}
|
||||||
result.mode ??= {}
|
result.mode ??= {}
|
||||||
result.plugin ??= []
|
result.plugin ??= []
|
||||||
@@ -141,13 +141,21 @@ export namespace Config {
|
|||||||
|
|
||||||
if (!result.keybinds) result.keybinds = Info.shape.keybinds.parse({})
|
if (!result.keybinds) result.keybinds = Info.shape.keybinds.parse({})
|
||||||
|
|
||||||
|
// Apply flag overrides for compaction settings
|
||||||
|
if (Flag.OPENCODE_DISABLE_AUTOCOMPACT) {
|
||||||
|
result.compaction = { ...result.compaction, auto: false }
|
||||||
|
}
|
||||||
|
if (Flag.OPENCODE_DISABLE_PRUNE) {
|
||||||
|
result.compaction = { ...result.compaction, prune: false }
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
config: result,
|
config: result,
|
||||||
directories,
|
directories,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const INVALID_DIRS = new Bun.Glob(`{${["agents", "commands", "plugins", "tools"].join(",")}}/`)
|
const INVALID_DIRS = new Bun.Glob(`{${["agents", "commands", "plugins", "tools", "skills"].join(",")}}/`)
|
||||||
async function assertValid(dir: string) {
|
async function assertValid(dir: string) {
|
||||||
const invalid = await Array.fromAsync(
|
const invalid = await Array.fromAsync(
|
||||||
INVALID_DIRS.scan({
|
INVALID_DIRS.scan({
|
||||||
@@ -183,6 +191,10 @@ export namespace Config {
|
|||||||
cwd: dir,
|
cwd: dir,
|
||||||
},
|
},
|
||||||
).catch(() => {})
|
).catch(() => {})
|
||||||
|
|
||||||
|
// Install any additional dependencies defined in the package.json
|
||||||
|
// This allows local plugins and custom tools to use external packages
|
||||||
|
await BunProc.run(["install"], { cwd: dir }).catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
const COMMAND_GLOB = new Bun.Glob("command/**/*.md")
|
const COMMAND_GLOB = new Bun.Glob("command/**/*.md")
|
||||||
@@ -587,6 +599,17 @@ export namespace Config {
|
|||||||
.describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"),
|
.describe("Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column"),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const Server = z
|
||||||
|
.object({
|
||||||
|
port: z.number().int().positive().optional().describe("Port to listen on"),
|
||||||
|
hostname: z.string().optional().describe("Hostname to listen on"),
|
||||||
|
mdns: z.boolean().optional().describe("Enable mDNS service discovery"),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
.meta({
|
||||||
|
ref: "ServerConfig",
|
||||||
|
})
|
||||||
|
|
||||||
export const Layout = z.enum(["auto", "stretch"]).meta({
|
export const Layout = z.enum(["auto", "stretch"]).meta({
|
||||||
ref: "LayoutConfig",
|
ref: "LayoutConfig",
|
||||||
})
|
})
|
||||||
@@ -633,7 +656,9 @@ export namespace Config {
|
|||||||
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
|
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
|
||||||
theme: z.string().optional().describe("Theme name to use for the interface"),
|
theme: z.string().optional().describe("Theme name to use for the interface"),
|
||||||
keybinds: Keybinds.optional().describe("Custom keybind configurations"),
|
keybinds: Keybinds.optional().describe("Custom keybind configurations"),
|
||||||
|
logLevel: Log.Level.optional().describe("Log level"),
|
||||||
tui: TUI.optional().describe("TUI specific settings"),
|
tui: TUI.optional().describe("TUI specific settings"),
|
||||||
|
server: Server.optional().describe("Server configuration for opencode serve and web commands"),
|
||||||
command: z
|
command: z
|
||||||
.record(z.string(), Command)
|
.record(z.string(), Command)
|
||||||
.optional()
|
.optional()
|
||||||
@@ -778,6 +803,12 @@ export namespace Config {
|
|||||||
url: z.string().optional().describe("Enterprise URL"),
|
url: z.string().optional().describe("Enterprise URL"),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
compaction: z
|
||||||
|
.object({
|
||||||
|
auto: z.boolean().optional().describe("Enable automatic compaction when context is full (default: true)"),
|
||||||
|
prune: z.boolean().optional().describe("Enable pruning of old tool outputs (default: true)"),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
experimental: z
|
experimental: z
|
||||||
.object({
|
.object({
|
||||||
hook: z
|
hook: z
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import path from "path"
|
|||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import ignore from "ignore"
|
import ignore from "ignore"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
|
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"
|
||||||
@@ -235,6 +236,13 @@ export namespace File {
|
|||||||
using _ = log.time("read", { file })
|
using _ = log.time("read", { file })
|
||||||
const project = Instance.project
|
const project = Instance.project
|
||||||
const full = path.join(Instance.directory, file)
|
const full = path.join(Instance.directory, file)
|
||||||
|
|
||||||
|
// TODO: Filesystem.contains is lexical only - symlinks inside the project can escape.
|
||||||
|
// TODO: On Windows, cross-drive paths bypass this check. Consider realpath canonicalization.
|
||||||
|
if (!Filesystem.contains(Instance.directory, full)) {
|
||||||
|
throw new Error(`Access denied: path escapes project directory`)
|
||||||
|
}
|
||||||
|
|
||||||
const bunFile = Bun.file(full)
|
const bunFile = Bun.file(full)
|
||||||
|
|
||||||
if (!(await bunFile.exists())) {
|
if (!(await bunFile.exists())) {
|
||||||
@@ -288,6 +296,13 @@ export namespace File {
|
|||||||
ignored = ig.ignores.bind(ig)
|
ignored = ig.ignores.bind(ig)
|
||||||
}
|
}
|
||||||
const resolved = dir ? path.join(Instance.directory, dir) : Instance.directory
|
const resolved = dir ? path.join(Instance.directory, dir) : Instance.directory
|
||||||
|
|
||||||
|
// TODO: Filesystem.contains is lexical only - symlinks inside the project can escape.
|
||||||
|
// TODO: On Windows, cross-drive paths bypass this check. Consider realpath canonicalization.
|
||||||
|
if (!Filesystem.contains(Instance.directory, resolved)) {
|
||||||
|
throw new Error(`Access denied: path escapes project directory`)
|
||||||
|
}
|
||||||
|
|
||||||
const nodes: Node[] = []
|
const nodes: Node[] = []
|
||||||
for (const entry of await fs.promises
|
for (const entry of await fs.promises
|
||||||
.readdir(resolved, {
|
.readdir(resolved, {
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -313,3 +313,21 @@ export const gleam: Info = {
|
|||||||
return Bun.which("gleam") !== null
|
return Bun.which("gleam") !== null
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const shfmt: Info = {
|
||||||
|
name: "shfmt",
|
||||||
|
command: ["shfmt", "-w", "$FILE"],
|
||||||
|
extensions: [".sh", ".bash"],
|
||||||
|
async enabled() {
|
||||||
|
return Bun.which("shfmt") !== null
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const nixfmt: Info = {
|
||||||
|
name: "nixfmt",
|
||||||
|
command: ["nixfmt", "$FILE"],
|
||||||
|
extensions: [".nix"],
|
||||||
|
async enabled() {
|
||||||
|
return Bun.which("nixfmt") !== null
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ const cli = yargs(hideBin(process.argv))
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.usage("\n" + UI.logo())
|
.usage("\n" + UI.logo())
|
||||||
|
.completion("completion", "generate shell completion script")
|
||||||
.command(AcpCommand)
|
.command(AcpCommand)
|
||||||
.command(McpCommand)
|
.command(McpCommand)
|
||||||
.command(TuiThreadCommand)
|
.command(TuiThreadCommand)
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ export namespace Installation {
|
|||||||
|
|
||||||
export async function latest(installMethod?: Method) {
|
export async function latest(installMethod?: Method) {
|
||||||
const detectedMethod = installMethod || (await method())
|
const detectedMethod = installMethod || (await method())
|
||||||
|
|
||||||
if (detectedMethod === "brew") {
|
if (detectedMethod === "brew") {
|
||||||
const formula = await getBrewFormula()
|
const formula = await getBrewFormula()
|
||||||
if (formula === "opencode") {
|
if (formula === "opencode") {
|
||||||
@@ -178,19 +179,26 @@ export namespace Installation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const registry = await iife(async () => {
|
if (detectedMethod === "npm" || detectedMethod === "bun" || detectedMethod === "pnpm") {
|
||||||
const r = (await $`npm config get registry`.quiet().nothrow().text()).trim()
|
const registry = await iife(async () => {
|
||||||
const reg = r || "https://registry.npmjs.org"
|
const r = (await $`npm config get registry`.quiet().nothrow().text()).trim()
|
||||||
return reg.endsWith("/") ? reg.slice(0, -1) : reg
|
const reg = r || "https://registry.npmjs.org"
|
||||||
})
|
return reg.endsWith("/") ? reg.slice(0, -1) : reg
|
||||||
const [major] = VERSION.split(".").map((x) => Number(x))
|
})
|
||||||
// const channel = CHANNEL === "latest" ? `latest-${major}` : CHANNEL
|
const channel = CHANNEL
|
||||||
const channel = CHANNEL
|
return fetch(`${registry}/opencode-ai/${channel}`)
|
||||||
return fetch(`${registry}/opencode-ai/${channel}`)
|
.then((res) => {
|
||||||
|
if (!res.ok) throw new Error(res.statusText)
|
||||||
|
return res.json()
|
||||||
|
})
|
||||||
|
.then((data: any) => data.version)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch("https://api.github.com/repos/sst/opencode/releases/latest")
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (!res.ok) throw new Error(res.statusText)
|
if (!res.ok) throw new Error(res.statusText)
|
||||||
return res.json()
|
return res.json()
|
||||||
})
|
})
|
||||||
.then((data: any) => data.version)
|
.then((data: any) => data.tag_name.replace(/^v/, ""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export const LANGUAGE_EXTENSIONS: Record<string, string> = {
|
|||||||
".hbs": "handlebars",
|
".hbs": "handlebars",
|
||||||
".handlebars": "handlebars",
|
".handlebars": "handlebars",
|
||||||
".hs": "haskell",
|
".hs": "haskell",
|
||||||
|
".lhs": "haskell",
|
||||||
".html": "html",
|
".html": "html",
|
||||||
".htm": "html",
|
".htm": "html",
|
||||||
".ini": "ini",
|
".ini": "ini",
|
||||||
|
|||||||
@@ -196,13 +196,14 @@ export namespace LSPServer {
|
|||||||
}
|
}
|
||||||
await fs.rename(extractedPath, finalPath)
|
await fs.rename(extractedPath, finalPath)
|
||||||
|
|
||||||
await $`npm install`.cwd(finalPath).quiet()
|
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm"
|
||||||
await $`npm run compile`.cwd(finalPath).quiet()
|
await $`${npmCmd} install`.cwd(finalPath).quiet()
|
||||||
|
await $`${npmCmd} run compile`.cwd(finalPath).quiet()
|
||||||
|
|
||||||
log.info("installed VS Code ESLint server", { serverPath })
|
log.info("installed VS Code ESLint server", { serverPath })
|
||||||
}
|
}
|
||||||
|
|
||||||
const proc = spawn(BunProc.which(), ["--max-old-space-size=8192", serverPath, "--stdio"], {
|
const proc = spawn(BunProc.which(), [serverPath, "--stdio"], {
|
||||||
cwd: root,
|
cwd: root,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
@@ -1175,7 +1176,7 @@ export namespace LSPServer {
|
|||||||
case "linux":
|
case "linux":
|
||||||
return "config_linux"
|
return "config_linux"
|
||||||
case "win32":
|
case "win32":
|
||||||
return "config_windows"
|
return "config_win"
|
||||||
default:
|
default:
|
||||||
return "config_linux"
|
return "config_linux"
|
||||||
}
|
}
|
||||||
@@ -1892,4 +1893,22 @@ export namespace LSPServer {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const HLS: Info = {
|
||||||
|
id: "haskell-language-server",
|
||||||
|
extensions: [".hs", ".lhs"],
|
||||||
|
root: NearestRoot(["stack.yaml", "cabal.project", "hie.yaml", "*.cabal"]),
|
||||||
|
async spawn(root) {
|
||||||
|
const bin = Bun.which("haskell-language-server-wrapper")
|
||||||
|
if (!bin) {
|
||||||
|
log.info("haskell-language-server-wrapper not found, please install haskell-language-server")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
process: spawn(bin, ["--lsp"], {
|
||||||
|
cwd: root,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -308,10 +308,12 @@ export namespace MCP {
|
|||||||
|
|
||||||
if (mcp.type === "local") {
|
if (mcp.type === "local") {
|
||||||
const [cmd, ...args] = mcp.command
|
const [cmd, ...args] = mcp.command
|
||||||
|
const cwd = Instance.directory
|
||||||
const transport = new StdioClientTransport({
|
const transport = new StdioClientTransport({
|
||||||
stderr: "ignore",
|
stderr: "ignore",
|
||||||
command: cmd,
|
command: cmd,
|
||||||
args,
|
args,
|
||||||
|
cwd,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
...(cmd === "opencode" ? { BUN_BE_BUN: "1" } : {}),
|
...(cmd === "opencode" ? { BUN_BE_BUN: "1" } : {}),
|
||||||
@@ -334,6 +336,7 @@ export namespace MCP {
|
|||||||
log.error("local mcp startup failed", {
|
log.error("local mcp startup failed", {
|
||||||
key,
|
key,
|
||||||
command: mcp.command,
|
command: mcp.command,
|
||||||
|
cwd,
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: error instanceof Error ? error.message : String(error),
|
||||||
})
|
})
|
||||||
status = {
|
status = {
|
||||||
|
|||||||
@@ -86,6 +86,17 @@ export namespace Permission {
|
|||||||
return state().pending
|
return state().pending
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function list() {
|
||||||
|
const { pending } = state()
|
||||||
|
const result: Info[] = []
|
||||||
|
for (const items of Object.values(pending)) {
|
||||||
|
for (const item of Object.values(items)) {
|
||||||
|
result.push(item.info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.sort((a, b) => a.id.localeCompare(b.id))
|
||||||
|
}
|
||||||
|
|
||||||
export async function ask(input: {
|
export async function ask(input: {
|
||||||
type: Info["type"]
|
type: Info["type"]
|
||||||
title: Info["title"]
|
title: Info["title"]
|
||||||
|
|||||||
@@ -165,29 +165,44 @@ export namespace Provider {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"amazon-bedrock": async () => {
|
"amazon-bedrock": async () => {
|
||||||
const [awsProfile, awsAccessKeyId, awsBearerToken, awsRegion] = await Promise.all([
|
const auth = await Auth.get("amazon-bedrock")
|
||||||
Env.get("AWS_PROFILE"),
|
const awsProfile = Env.get("AWS_PROFILE")
|
||||||
Env.get("AWS_ACCESS_KEY_ID"),
|
const awsAccessKeyId = Env.get("AWS_ACCESS_KEY_ID")
|
||||||
Env.get("AWS_BEARER_TOKEN_BEDROCK"),
|
const awsRegion = Env.get("AWS_REGION")
|
||||||
Env.get("AWS_REGION"),
|
|
||||||
])
|
const awsBearerToken = iife(() => {
|
||||||
|
const envToken = Env.get("AWS_BEARER_TOKEN_BEDROCK")
|
||||||
|
if (envToken) return envToken
|
||||||
|
if (auth?.type === "api") {
|
||||||
|
Env.set("AWS_BEARER_TOKEN_BEDROCK", auth.key)
|
||||||
|
return auth.key
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
|
||||||
if (!awsProfile && !awsAccessKeyId && !awsBearerToken) return { autoload: false }
|
if (!awsProfile && !awsAccessKeyId && !awsBearerToken) return { autoload: false }
|
||||||
|
|
||||||
const region = awsRegion ?? "us-east-1"
|
const defaultRegion = awsRegion ?? "us-east-1"
|
||||||
|
|
||||||
const { fromNodeProviderChain } = await import(await BunProc.install("@aws-sdk/credential-providers"))
|
const { fromNodeProviderChain } = await import(await BunProc.install("@aws-sdk/credential-providers"))
|
||||||
return {
|
return {
|
||||||
autoload: true,
|
autoload: true,
|
||||||
options: {
|
options: {
|
||||||
region,
|
region: defaultRegion,
|
||||||
credentialProvider: fromNodeProviderChain(),
|
credentialProvider: fromNodeProviderChain(),
|
||||||
},
|
},
|
||||||
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
|
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
|
||||||
// Skip region prefixing if model already has global prefix
|
// Skip region prefixing if model already has global prefix
|
||||||
if (modelID.startsWith("global.")) {
|
if (modelID.startsWith("global.")) {
|
||||||
return sdk.languageModel(modelID)
|
return sdk.languageModel(modelID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Region resolution precedence (highest to lowest):
|
||||||
|
// 1. options.region from opencode.json provider config
|
||||||
|
// 2. defaultRegion from AWS_REGION environment variable
|
||||||
|
// 3. Default "us-east-1" (baked into defaultRegion)
|
||||||
|
const region = options?.region ?? defaultRegion
|
||||||
|
|
||||||
let regionPrefix = region.split("-")[0]
|
let regionPrefix = region.split("-")[0]
|
||||||
|
|
||||||
switch (regionPrefix) {
|
switch (regionPrefix) {
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ export namespace ProviderTransform {
|
|||||||
const id = model.id.toLowerCase()
|
const id = model.id.toLowerCase()
|
||||||
if (id.includes("qwen")) return 0.55
|
if (id.includes("qwen")) return 0.55
|
||||||
if (id.includes("claude")) return undefined
|
if (id.includes("claude")) return undefined
|
||||||
if (id.includes("gemini-3-pro")) return 1.0
|
if (id.includes("gemini")) return 1.0
|
||||||
if (id.includes("glm-4.6")) return 1.0
|
if (id.includes("glm-4.6")) return 1.0
|
||||||
if (id.includes("glm-4.7")) return 1.0
|
if (id.includes("glm-4.7")) return 1.0
|
||||||
if (id.includes("minimax-m2")) return 1.0
|
if (id.includes("minimax-m2")) return 1.0
|
||||||
@@ -232,12 +232,14 @@ export namespace ProviderTransform {
|
|||||||
if (id.includes("m2.1")) return 0.9
|
if (id.includes("m2.1")) return 0.9
|
||||||
return 0.95
|
return 0.95
|
||||||
}
|
}
|
||||||
|
if (id.includes("gemini")) return 0.95
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export function topK(model: Provider.Model) {
|
export function topK(model: Provider.Model) {
|
||||||
const id = model.id.toLowerCase()
|
const id = model.id.toLowerCase()
|
||||||
if (id.includes("minimax-m2")) return 20
|
if (id.includes("minimax-m2")) return 20
|
||||||
|
if (id.includes("gemini")) return 64
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,22 +302,22 @@ 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 }) {
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { Log } from "@/util/log"
|
||||||
|
import Bonjour from "bonjour-service"
|
||||||
|
|
||||||
|
const log = Log.create({ service: "mdns" })
|
||||||
|
|
||||||
|
export namespace MDNS {
|
||||||
|
let bonjour: Bonjour | undefined
|
||||||
|
let currentPort: number | undefined
|
||||||
|
|
||||||
|
export function publish(port: number, name = "opencode") {
|
||||||
|
if (currentPort === port) return
|
||||||
|
if (bonjour) unpublish()
|
||||||
|
|
||||||
|
try {
|
||||||
|
bonjour = new Bonjour()
|
||||||
|
const service = bonjour.publish({
|
||||||
|
name,
|
||||||
|
type: "http",
|
||||||
|
port,
|
||||||
|
txt: { path: "/" },
|
||||||
|
})
|
||||||
|
|
||||||
|
service.on("up", () => {
|
||||||
|
log.info("mDNS service published", { name, port })
|
||||||
|
})
|
||||||
|
|
||||||
|
service.on("error", (err) => {
|
||||||
|
log.error("mDNS service error", { error: err })
|
||||||
|
})
|
||||||
|
|
||||||
|
currentPort = port
|
||||||
|
} catch (err) {
|
||||||
|
log.error("mDNS publish failed", { error: err })
|
||||||
|
if (bonjour) {
|
||||||
|
try {
|
||||||
|
bonjour.destroy()
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
bonjour = undefined
|
||||||
|
currentPort = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unpublish() {
|
||||||
|
if (bonjour) {
|
||||||
|
try {
|
||||||
|
bonjour.unpublishAll()
|
||||||
|
bonjour.destroy()
|
||||||
|
} catch (err) {
|
||||||
|
log.error("mDNS unpublish failed", { error: err })
|
||||||
|
}
|
||||||
|
bonjour = undefined
|
||||||
|
currentPort = undefined
|
||||||
|
log.info("mDNS service unpublished")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,9 +45,11 @@ import { Snapshot } from "@/snapshot"
|
|||||||
import { SessionSummary } from "@/session/summary"
|
import { SessionSummary } from "@/session/summary"
|
||||||
import { SessionStatus } from "@/session/status"
|
import { SessionStatus } from "@/session/status"
|
||||||
import { upgradeWebSocket, websocket } from "hono/bun"
|
import { upgradeWebSocket, websocket } from "hono/bun"
|
||||||
|
import type { BunWebSocketData } from "hono/bun"
|
||||||
import { errors } from "./error"
|
import { errors } from "./error"
|
||||||
import { Pty } from "@/pty"
|
import { Pty } from "@/pty"
|
||||||
import { Installation } from "@/installation"
|
import { Installation } from "@/installation"
|
||||||
|
import { MDNS } from "./mdns"
|
||||||
|
|
||||||
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
|
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
|
||||||
globalThis.AI_SDK_LOG_WARNINGS = false
|
globalThis.AI_SDK_LOG_WARNINGS = false
|
||||||
@@ -1082,6 +1084,8 @@ export namespace Server {
|
|||||||
async (c) => {
|
async (c) => {
|
||||||
const sessionID = c.req.valid("param").sessionID
|
const sessionID = c.req.valid("param").sessionID
|
||||||
const body = c.req.valid("json")
|
const body = c.req.valid("json")
|
||||||
|
const session = await Session.get(sessionID)
|
||||||
|
await SessionRevert.cleanup(session)
|
||||||
const msgs = await Session.messages({ sessionID })
|
const msgs = await Session.messages({ sessionID })
|
||||||
let currentAgent = await Agent.defaultAgent()
|
let currentAgent = await Agent.defaultAgent()
|
||||||
for (let i = msgs.length - 1; i >= 0; i--) {
|
for (let i = msgs.length - 1; i >= 0; i--) {
|
||||||
@@ -1528,6 +1532,28 @@ export namespace Server {
|
|||||||
return c.json(true)
|
return c.json(true)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.get(
|
||||||
|
"/permission",
|
||||||
|
describeRoute({
|
||||||
|
summary: "List pending permissions",
|
||||||
|
description: "Get all pending permission requests across all sessions.",
|
||||||
|
operationId: "permission.list",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "List of pending permissions",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(Permission.Info.array()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
|
const permissions = Permission.list()
|
||||||
|
return c.json(permissions)
|
||||||
|
},
|
||||||
|
)
|
||||||
.get(
|
.get(
|
||||||
"/command",
|
"/command",
|
||||||
describeRoute({
|
describeRoute({
|
||||||
@@ -2623,20 +2649,41 @@ export namespace Server {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listen(opts: { port: number; hostname: string }) {
|
export function listen(opts: { port: number; hostname: string; mdns?: boolean }) {
|
||||||
const args = {
|
const args = {
|
||||||
hostname: opts.hostname,
|
hostname: opts.hostname,
|
||||||
idleTimeout: 0,
|
idleTimeout: 0,
|
||||||
fetch: App().fetch,
|
fetch: App().fetch,
|
||||||
websocket: websocket,
|
websocket: websocket,
|
||||||
} as const
|
} as const
|
||||||
if (opts.port === 0) {
|
const tryServe = (port: number) => {
|
||||||
try {
|
try {
|
||||||
return Bun.serve({ ...args, port: 4096 })
|
return Bun.serve({ ...args, port })
|
||||||
} catch {
|
} catch {
|
||||||
// port 4096 not available, fall through to use port 0
|
return undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Bun.serve({ ...args, port: 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}`)
|
||||||
|
|
||||||
|
const shouldPublishMDNS =
|
||||||
|
opts.mdns &&
|
||||||
|
server.port &&
|
||||||
|
opts.hostname !== "127.0.0.1" &&
|
||||||
|
opts.hostname !== "localhost" &&
|
||||||
|
opts.hostname !== "::1"
|
||||||
|
if (shouldPublishMDNS) {
|
||||||
|
MDNS.publish(server.port!)
|
||||||
|
} else if (opts.mdns) {
|
||||||
|
log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish")
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalStop = server.stop.bind(server)
|
||||||
|
server.stop = async (closeActiveConnections?: boolean) => {
|
||||||
|
if (shouldPublishMDNS) MDNS.unpublish()
|
||||||
|
return originalStop(closeActiveConnections)
|
||||||
|
}
|
||||||
|
|
||||||
|
return server
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import { Provider } from "../provider/provider"
|
|||||||
import { MessageV2 } from "./message-v2"
|
import { MessageV2 } from "./message-v2"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { SessionPrompt } from "./prompt"
|
import { SessionPrompt } from "./prompt"
|
||||||
import { Flag } from "../flag/flag"
|
|
||||||
import { Token } from "../util/token"
|
import { Token } from "../util/token"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { SessionProcessor } from "./processor"
|
import { SessionProcessor } from "./processor"
|
||||||
import { fn } from "@/util/fn"
|
import { fn } from "@/util/fn"
|
||||||
import { Agent } from "@/agent/agent"
|
import { Agent } from "@/agent/agent"
|
||||||
import { Plugin } from "@/plugin"
|
import { Plugin } from "@/plugin"
|
||||||
|
import { Config } from "@/config/config"
|
||||||
|
|
||||||
export namespace SessionCompaction {
|
export namespace SessionCompaction {
|
||||||
const log = Log.create({ service: "session.compaction" })
|
const log = Log.create({ service: "session.compaction" })
|
||||||
@@ -27,8 +27,9 @@ export namespace SessionCompaction {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isOverflow(input: { tokens: MessageV2.Assistant["tokens"]; model: Provider.Model }) {
|
export async function isOverflow(input: { tokens: MessageV2.Assistant["tokens"]; model: Provider.Model }) {
|
||||||
if (Flag.OPENCODE_DISABLE_AUTOCOMPACT) return false
|
const config = await Config.get()
|
||||||
|
if (config.compaction?.auto === false) return false
|
||||||
const context = input.model.limit.context
|
const context = input.model.limit.context
|
||||||
if (context === 0) return false
|
if (context === 0) return false
|
||||||
const count = input.tokens.input + input.tokens.cache.read + input.tokens.output
|
const count = input.tokens.input + input.tokens.cache.read + input.tokens.output
|
||||||
@@ -46,7 +47,8 @@ export namespace SessionCompaction {
|
|||||||
// calls. then erases output of previous tool calls. idea is to throw away old
|
// calls. then erases output of previous tool calls. idea is to throw away old
|
||||||
// tool calls that are no longer relevant.
|
// tool calls that are no longer relevant.
|
||||||
export async function prune(input: { sessionID: string }) {
|
export async function prune(input: { sessionID: string }) {
|
||||||
if (Flag.OPENCODE_DISABLE_PRUNE) return
|
const config = await Config.get()
|
||||||
|
if (config.compaction?.prune === false) return
|
||||||
log.info("pruning")
|
log.info("pruning")
|
||||||
const msgs = await Session.messages({ sessionID: input.sessionID })
|
const msgs = await Session.messages({ sessionID: input.sessionID })
|
||||||
let total = 0
|
let total = 0
|
||||||
|
|||||||
@@ -539,7 +539,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) {
|
||||||
|
|||||||
@@ -459,7 +459,7 @@ export namespace SessionPrompt {
|
|||||||
if (
|
if (
|
||||||
lastFinished &&
|
lastFinished &&
|
||||||
lastFinished.summary !== true &&
|
lastFinished.summary !== true &&
|
||||||
SessionCompaction.isOverflow({ tokens: lastFinished.tokens, model })
|
(await SessionCompaction.isOverflow({ tokens: lastFinished.tokens, model }))
|
||||||
) {
|
) {
|
||||||
await SessionCompaction.create({
|
await SessionCompaction.create({
|
||||||
sessionID,
|
sessionID,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user