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 |
@@ -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 }}
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ on:
|
|||||||
- cron: "30 1 * * *" # Daily at 1:30 AM
|
- cron: "30 1 * * *" # Daily at 1:30 AM
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
DAYS_BEFORE_STALE: 90
|
||||||
|
DAYS_BEFORE_CLOSE: 7
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -13,17 +17,17 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v10
|
- uses: actions/stale@v10
|
||||||
with:
|
with:
|
||||||
days-before-stale: 90
|
days-before-stale: ${{ env.DAYS_BEFORE_STALE }}
|
||||||
days-before-close: 7
|
days-before-close: ${{ env.DAYS_BEFORE_CLOSE }}
|
||||||
stale-issue-label: "stale"
|
stale-issue-label: "stale"
|
||||||
close-issue-message: |
|
close-issue-message: |
|
||||||
[automated] Closing due to 90+ days of inactivity.
|
[automated] Closing due to ${{ env.DAYS_BEFORE_STALE }}+ days of inactivity.
|
||||||
|
|
||||||
Feel free to reopen if you still need this!
|
Feel free to reopen if you still need this!
|
||||||
stale-issue-message: |
|
stale-issue-message: |
|
||||||
[automated] This issue has had no activity for 90 days.
|
[automated] This issue has had no activity for ${{ env.DAYS_BEFORE_STALE }} days.
|
||||||
|
|
||||||
It will be closed in 7 days if there's no new activity.
|
It will be closed in ${{ env.DAYS_BEFORE_CLOSE }} days if there's no new activity.
|
||||||
remove-stale-when-updated: true
|
remove-stale-when-updated: true
|
||||||
exempt-issue-labels: "pinned,security,feature-request,on-hold"
|
exempt-issue-labels: "pinned,security,feature-request,on-hold"
|
||||||
start-date: "2025-12-27"
|
start-date: "2025-12-27"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -183,3 +183,5 @@
|
|||||||
| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) |
|
| 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-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-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) |
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@opencode-ai/app",
|
"name": "@opencode-ai/app",
|
||||||
"version": "1.0.204",
|
"version": "1.0.209",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.0.204",
|
"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",
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.0.204",
|
"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",
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.0.204",
|
"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",
|
||||||
@@ -149,7 +149,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.0.204",
|
"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",
|
||||||
@@ -173,7 +173,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.0.204",
|
"version": "1.0.209",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/app": "workspace:*",
|
"@opencode-ai/app": "workspace:*",
|
||||||
"@solid-primitives/storage": "catalog:",
|
"@solid-primitives/storage": "catalog:",
|
||||||
@@ -200,7 +200,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.0.204",
|
"version": "1.0.209",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
@@ -229,7 +229,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.0.204",
|
"version": "1.0.209",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "catalog:",
|
"@octokit/rest": "catalog:",
|
||||||
@@ -245,7 +245,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.0.204",
|
"version": "1.0.209",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -254,7 +254,7 @@
|
|||||||
"@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/cerebras": "1.0.33",
|
||||||
"@ai-sdk/cohere": "2.0.21",
|
"@ai-sdk/cohere": "2.0.21",
|
||||||
@@ -269,7 +269,7 @@
|
|||||||
"@ai-sdk/openai-compatible": "1.0.27",
|
"@ai-sdk/openai-compatible": "1.0.27",
|
||||||
"@ai-sdk/perplexity": "2.0.22",
|
"@ai-sdk/perplexity": "2.0.22",
|
||||||
"@ai-sdk/provider": "2.0.0",
|
"@ai-sdk/provider": "2.0.0",
|
||||||
"@ai-sdk/provider-utils": "3.0.18",
|
"@ai-sdk/provider-utils": "3.0.19",
|
||||||
"@ai-sdk/togetherai": "1.0.30",
|
"@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",
|
||||||
@@ -347,7 +347,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.0.204",
|
"version": "1.0.209",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
@@ -367,7 +367,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.0.204",
|
"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:",
|
||||||
@@ -378,7 +378,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.0.204",
|
"version": "1.0.209",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -391,7 +391,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.0.204",
|
"version": "1.0.209",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -403,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:",
|
||||||
@@ -416,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:",
|
||||||
@@ -426,7 +429,7 @@
|
|||||||
},
|
},
|
||||||
"packages/util": {
|
"packages/util": {
|
||||||
"name": "@opencode-ai/util",
|
"name": "@opencode-ai/util",
|
||||||
"version": "1.0.204",
|
"version": "1.0.209",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -437,7 +440,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.0.204",
|
"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",
|
||||||
@@ -566,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=="],
|
||||||
|
|
||||||
@@ -1774,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=="],
|
||||||
@@ -2786,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=="],
|
||||||
@@ -2876,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=="],
|
||||||
@@ -3898,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=="],
|
||||||
@@ -4284,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=="],
|
||||||
@@ -4308,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": 1766747458,
|
"lastModified": 1766996594,
|
||||||
"narHash": "sha256-m63jjuo/ygo8ztkCziYh5OOIbTSXUDkKbqw3Vuqu4a4=",
|
"narHash": "sha256-SosfgQSqVmOkqVgNYJnxW5FvoIQX4grOcpIKNrIwz4o=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "c633f572eded8c4f3c75b8010129854ed404a6ce",
|
"rev": "0744ef1b047f07d31d9962d757ffe38ec14a4d41",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": "sha256-lloUZt5mLyNWAcbQrJB4wGUKvKu24WFEhOLfZD5/FMg="
|
"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)
|
||||||
|
|||||||
+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.204",
|
"version": "1.0.209",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
+45
-42
@@ -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"
|
||||||
@@ -45,48 +46,50 @@ 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -248,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
|
||||||
|
|
||||||
@@ -270,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) {
|
||||||
@@ -278,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)
|
||||||
|
|
||||||
@@ -302,9 +302,15 @@ 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(() => {
|
||||||
@@ -1024,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,
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -35,19 +35,13 @@ export function SessionContextUsage() {
|
|||||||
{(ctx) => (
|
{(ctx) => (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
value={
|
value={
|
||||||
<div class="flex flex-col gap-1">
|
<div class="grid grid-cols-2 gap-x-3 gap-y-1">
|
||||||
<div class="flex gap-3">
|
<span class="opacity-70 text-right">Tokens</span>
|
||||||
<span class="opacity-70 text-right flex-1">Tokens</span>
|
<span class="text-left">{ctx().tokens}</span>
|
||||||
<span class="text-left flex-1">{ctx().tokens}</span>
|
<span class="opacity-70 text-right">Usage</span>
|
||||||
</div>
|
<span class="text-left">{ctx().percentage ?? 0}%</span>
|
||||||
<div class="flex gap-3">
|
<span class="opacity-70 text-right">Cost</span>
|
||||||
<span class="opacity-70 text-right flex-1">Usage</span>
|
<span class="text-left">{cost()}</span>
|
||||||
<span class="text-left flex-1">{ctx().percentage ?? 0}%</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-3">
|
|
||||||
<span class="opacity-70 text-right flex-1">Cost</span>
|
|
||||||
<span class="text-left flex-1">{cost()}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
placement="top"
|
placement="top"
|
||||||
|
|||||||
@@ -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,8 +153,6 @@ 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)
|
||||||
@@ -134,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({
|
||||||
@@ -153,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">
|
||||||
|
|||||||
@@ -23,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"
|
||||||
|
|
||||||
@@ -71,22 +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>>> = {}
|
||||||
const permissionListeners: Set<(info: { directory: string; permission: Permission }) => void> = new Set()
|
|
||||||
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: {},
|
||||||
@@ -106,7 +103,6 @@ function createGlobalSync() {
|
|||||||
message: {},
|
message: {},
|
||||||
part: {},
|
part: {},
|
||||||
})
|
})
|
||||||
children[directory] = createStore(globalStore.children[directory])
|
|
||||||
bootstrapInstance(directory)
|
bootstrapInstance(directory)
|
||||||
}
|
}
|
||||||
return children[directory]
|
return children[directory]
|
||||||
@@ -128,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)
|
||||||
@@ -139,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,
|
||||||
@@ -171,12 +167,32 @@ function createGlobalSync() {
|
|||||||
vcs: () => sdk.vcs.get().then((x) => setStore("vcs", x.data)),
|
vcs: () => sdk.vcs.get().then((x) => setStore("vcs", x.data)),
|
||||||
permission: () =>
|
permission: () =>
|
||||||
sdk.permission.list().then((x) => {
|
sdk.permission.list().then((x) => {
|
||||||
const grouped: Record<string, typeof x.data> = {}
|
const grouped: Record<string, Permission[]> = {}
|
||||||
for (const perm of x.data ?? []) {
|
for (const perm of x.data ?? []) {
|
||||||
grouped[perm.sessionID] = grouped[perm.sessionID] ?? []
|
const existing = grouped[perm.sessionID]
|
||||||
grouped[perm.sessionID]!.push(perm)
|
if (existing) {
|
||||||
|
existing.push(perm)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
grouped[perm.sessionID] = [perm]
|
||||||
}
|
}
|
||||||
setStore("permission", grouped)
|
|
||||||
|
batch(() => {
|
||||||
|
for (const sessionID of Object.keys(store.permission)) {
|
||||||
|
if (grouped[sessionID]) continue
|
||||||
|
setStore("permission", sessionID, [])
|
||||||
|
}
|
||||||
|
for (const [sessionID, permissions] of Object.entries(grouped)) {
|
||||||
|
setStore(
|
||||||
|
"permission",
|
||||||
|
sessionID,
|
||||||
|
reconcile(
|
||||||
|
permissions.slice().sort((a, b) => a.id.localeCompare(b.id)),
|
||||||
|
{ key: "id" },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
await Promise.all(Object.values(load).map((p) => retry(p).catch((e) => setGlobalStore("error", e))))
|
await Promise.all(Object.values(load).map((p) => retry(p).catch((e) => setGlobalStore("error", e))))
|
||||||
@@ -247,7 +263,7 @@ function createGlobalSync() {
|
|||||||
setStore("session_diff", event.properties.sessionID, reconcile(event.properties.diff, { key: "file" }))
|
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, reconcile(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, reconcile(event.properties.status))
|
setStore("session_status", event.properties.sessionID, reconcile(event.properties.status))
|
||||||
@@ -329,29 +345,26 @@ function createGlobalSync() {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "permission.updated": {
|
case "permission.updated": {
|
||||||
const permissions = store.permission[event.properties.sessionID]
|
const sessionID = event.properties.sessionID
|
||||||
const isNew = !permissions || !permissions.find((p) => p.id === event.properties.id)
|
const permissions = store.permission[sessionID]
|
||||||
if (!permissions) {
|
if (!permissions) {
|
||||||
setStore("permission", event.properties.sessionID, [event.properties])
|
setStore("permission", sessionID, [event.properties])
|
||||||
} else {
|
break
|
||||||
const result = Binary.search(permissions, event.properties.id, (p) => p.id)
|
|
||||||
setStore(
|
|
||||||
"permission",
|
|
||||||
event.properties.sessionID,
|
|
||||||
produce((draft) => {
|
|
||||||
if (result.found) {
|
|
||||||
draft[result.index] = event.properties
|
|
||||||
return
|
|
||||||
}
|
|
||||||
draft.push(event.properties)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (isNew) {
|
|
||||||
for (const listener of permissionListeners) {
|
const result = Binary.search(permissions, event.properties.id, (p) => p.id)
|
||||||
listener({ directory, permission: event.properties })
|
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
|
break
|
||||||
}
|
}
|
||||||
case "permission.replied": {
|
case "permission.replied": {
|
||||||
@@ -368,6 +381,15 @@ function createGlobalSync() {
|
|||||||
)
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case "lsp.updated": {
|
||||||
|
const sdk = createOpencodeClient({
|
||||||
|
baseUrl: globalSDK.url,
|
||||||
|
directory,
|
||||||
|
throwOnError: true,
|
||||||
|
})
|
||||||
|
sdk.lsp.status().then((x) => setStore("lsp", x.data ?? []))
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -439,12 +461,6 @@ function createGlobalSync() {
|
|||||||
project: {
|
project: {
|
||||||
loadSessions,
|
loadSessions,
|
||||||
},
|
},
|
||||||
permission: {
|
|
||||||
onUpdated(listener: (info: { directory: string; permission: Permission }) => void) {
|
|
||||||
permissionListeners.add(listener)
|
|
||||||
return () => permissionListeners.delete(listener)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
{
|
{
|
||||||
...project,
|
...project,
|
||||||
...(metadata ?? {}),
|
...(metadata ?? {}),
|
||||||
|
icon: { url: metadata?.icon?.url, color: metadata?.icon?.color },
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useParams } from "@solidjs/router"
|
|||||||
import { SDKProvider, useSDK } 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"
|
||||||
@@ -19,16 +20,18 @@ export default function Layout(props: ParentProps) {
|
|||||||
{iife(() => {
|
{iife(() => {
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
|
const respond = (input: {
|
||||||
|
sessionID: string
|
||||||
|
permissionID: string
|
||||||
|
response: "once" | "always" | "reject"
|
||||||
|
}) => sdk.client.permission.respond(input)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataProvider
|
<PermissionProvider permissions={sync.data.permission} onRespond={respond}>
|
||||||
data={sync.data}
|
<DataProvider data={sync.data} directory={directory()} onPermissionRespond={respond}>
|
||||||
directory={directory()}
|
<LocalProvider>{props.children}</LocalProvider>
|
||||||
onPermissionRespond={(input) => {
|
</DataProvider>
|
||||||
sdk.client.permission.respond(input)
|
</PermissionProvider>
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LocalProvider>{props.children}</LocalProvider>
|
|
||||||
</DataProvider>
|
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</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
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -41,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) {
|
||||||
@@ -89,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) {
|
||||||
@@ -118,7 +155,15 @@ export default function Layout(props: ParentProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const unsub = globalSync.permission.onUpdated(({ directory, permission }) => {
|
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 currentDir = params.dir ? base64Decode(params.dir) : undefined
|
||||||
const currentSession = params.id
|
const currentSession = params.id
|
||||||
if (directory === currentDir && permission.sessionID === currentSession) return
|
if (directory === currentDir && permission.sessionID === currentSession) return
|
||||||
@@ -127,7 +172,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
if (directory === currentDir && session?.parentID === currentSession) return
|
if (directory === currentDir && session?.parentID === currentSession) return
|
||||||
const sessionTitle = session?.title ?? "New session"
|
const sessionTitle = session?.title ?? "New session"
|
||||||
const projectName = getFilename(directory)
|
const projectName = getFilename(directory)
|
||||||
showToast({
|
const toastId = showToast({
|
||||||
persistent: true,
|
persistent: true,
|
||||||
icon: "checklist",
|
icon: "checklist",
|
||||||
title: "Permission required",
|
title: "Permission required",
|
||||||
@@ -138,7 +183,6 @@ export default function Layout(props: ParentProps) {
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
navigate(`/${base64Encode(directory)}/session/${permission.sessionID}`)
|
navigate(`/${base64Encode(directory)}/session/${permission.sessionID}`)
|
||||||
},
|
},
|
||||||
dismissAfter: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Dismiss",
|
label: "Dismiss",
|
||||||
@@ -146,8 +190,33 @@ export default function Layout(props: ParentProps) {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
toastBySession.set(sessionKey, toastId)
|
||||||
})
|
})
|
||||||
onCleanup(unsub)
|
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) {
|
||||||
@@ -254,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 />)
|
||||||
@@ -409,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"
|
||||||
|
|
||||||
@@ -445,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>
|
||||||
@@ -588,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))
|
||||||
@@ -634,6 +748,11 @@ 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>
|
||||||
@@ -797,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>
|
||||||
@@ -816,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"
|
||||||
|
|||||||
@@ -60,6 +60,14 @@ import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
|
|||||||
import { StatusBar } from "@/components/status-bar"
|
import { StatusBar } from "@/components/status-bar"
|
||||||
import { SessionMcpIndicator } from "@/components/session-mcp-indicator"
|
import { SessionMcpIndicator } from "@/components/session-mcp-indicator"
|
||||||
import { SessionLspIndicator } from "@/components/session-lsp-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()
|
||||||
@@ -74,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,
|
||||||
@@ -159,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) => {
|
||||||
@@ -304,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",
|
||||||
@@ -575,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)}
|
||||||
@@ -631,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)}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.0.204",
|
"version": "1.0.209",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo --noEmit",
|
||||||
|
|||||||
@@ -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.204",
|
"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.204",
|
"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",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.0.204",
|
"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.204",
|
"version": "1.0.209",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo -b",
|
"typecheck": "tsgo -b",
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ 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: {
|
build: {
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.0.204",
|
"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.204"
|
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.204/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.204/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.204/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.204/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.204/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.204",
|
"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.204",
|
"version": "1.0.209",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
"@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/cerebras": "1.0.33",
|
||||||
"@ai-sdk/cohere": "2.0.21",
|
"@ai-sdk/cohere": "2.0.21",
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
"@ai-sdk/openai-compatible": "1.0.27",
|
"@ai-sdk/openai-compatible": "1.0.27",
|
||||||
"@ai-sdk/perplexity": "2.0.22",
|
"@ai-sdk/perplexity": "2.0.22",
|
||||||
"@ai-sdk/provider": "2.0.0",
|
"@ai-sdk/provider": "2.0.0",
|
||||||
"@ai-sdk/provider-utils": "3.0.18",
|
"@ai-sdk/provider-utils": "3.0.19",
|
||||||
"@ai-sdk/togetherai": "1.0.30",
|
"@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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -158,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",
|
||||||
@@ -890,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() {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
@@ -784,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`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -812,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`
|
||||||
@@ -826,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()
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 ??= []
|
||||||
@@ -155,7 +155,7 @@ export namespace Config {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
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({
|
||||||
@@ -191,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")
|
||||||
|
|||||||
@@ -9,9 +9,13 @@ import path from "path"
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { createWrapper } from "@parcel/watcher/wrapper"
|
import { createWrapper } from "@parcel/watcher/wrapper"
|
||||||
import { lazy } from "@/util/lazy"
|
import { lazy } from "@/util/lazy"
|
||||||
|
import { withTimeout } from "@/util/timeout"
|
||||||
import type ParcelWatcher from "@parcel/watcher"
|
import type ParcelWatcher from "@parcel/watcher"
|
||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
import { Flag } from "@/flag/flag"
|
import { Flag } from "@/flag/flag"
|
||||||
|
import { readdir } from "fs/promises"
|
||||||
|
|
||||||
|
const SUBSCRIBE_TIMEOUT_MS = 10_000
|
||||||
|
|
||||||
declare const OPENCODE_LIBC: string | undefined
|
declare const OPENCODE_LIBC: string | undefined
|
||||||
|
|
||||||
@@ -63,12 +67,16 @@ export namespace FileWatcher {
|
|||||||
const cfgIgnores = cfg.watcher?.ignore ?? []
|
const cfgIgnores = cfg.watcher?.ignore ?? []
|
||||||
|
|
||||||
if (Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER) {
|
if (Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER) {
|
||||||
subs.push(
|
const pending = watcher().subscribe(Instance.directory, subscribe, {
|
||||||
await watcher().subscribe(Instance.directory, subscribe, {
|
ignore: [...FileIgnore.PATTERNS, ...cfgIgnores],
|
||||||
ignore: [...FileIgnore.PATTERNS, ...cfgIgnores],
|
backend,
|
||||||
backend,
|
})
|
||||||
}),
|
const sub = await withTimeout(pending, SUBSCRIBE_TIMEOUT_MS).catch((err) => {
|
||||||
)
|
log.error("failed to subscribe to Instance.directory", { error: err })
|
||||||
|
pending.then((s) => s.unsubscribe()).catch(() => {})
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
if (sub) subs.push(sub)
|
||||||
}
|
}
|
||||||
|
|
||||||
const vcsDir = await $`git rev-parse --git-dir`
|
const vcsDir = await $`git rev-parse --git-dir`
|
||||||
@@ -78,12 +86,18 @@ export namespace FileWatcher {
|
|||||||
.text()
|
.text()
|
||||||
.then((x) => path.resolve(Instance.worktree, x.trim()))
|
.then((x) => path.resolve(Instance.worktree, x.trim()))
|
||||||
if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) {
|
if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) {
|
||||||
subs.push(
|
const gitDirContents = await readdir(vcsDir).catch(() => [])
|
||||||
await watcher().subscribe(vcsDir, subscribe, {
|
const ignoreList = gitDirContents.filter((entry) => entry !== "HEAD")
|
||||||
ignore: ["hooks", "info", "logs", "objects", "refs", "worktrees", "modules", "lfs"],
|
const pending = watcher().subscribe(vcsDir, subscribe, {
|
||||||
backend,
|
ignore: ignoreList,
|
||||||
}),
|
backend,
|
||||||
)
|
})
|
||||||
|
const sub = await withTimeout(pending, SUBSCRIBE_TIMEOUT_MS).catch((err) => {
|
||||||
|
log.error("failed to subscribe to vcsDir", { error: err })
|
||||||
|
pending.then((s) => s.unsubscribe()).catch(() => {})
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
if (sub) subs.push(sub)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { subs }
|
return { subs }
|
||||||
|
|||||||
@@ -322,3 +322,12 @@ export const shfmt: Info = {
|
|||||||
return Bun.which("shfmt") !== null
|
return Bun.which("shfmt") !== null
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const nixfmt: Info = {
|
||||||
|
name: "nixfmt",
|
||||||
|
command: ["nixfmt", "$FILE"],
|
||||||
|
extensions: [".nix"],
|
||||||
|
async enabled() {
|
||||||
|
return Bun.which("nixfmt") !== null
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -302,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 }) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ Carefully read the issue and think hard about a plan to solve it before coding.
|
|||||||
- Use the `webfetch` tool to search google by fetching the URL `https://www.google.com/search?q=your+search+query`.
|
- Use the `webfetch` tool to search google by fetching the URL `https://www.google.com/search?q=your+search+query`.
|
||||||
- After fetching, review the content returned by the fetch tool.
|
- After fetching, review the content returned by the fetch tool.
|
||||||
- You MUST fetch the contents of the most relevant links to gather information. Do not rely on the summary that you find in the search results.
|
- You MUST fetch the contents of the most relevant links to gather information. Do not rely on the summary that you find in the search results.
|
||||||
- As you fetch each link, read the content thoroughly and fetch any additional links that you find withhin the content that are relevant to the problem.
|
- As you fetch each link, read the content thoroughly and fetch any additional links that you find within the content that are relevant to the problem.
|
||||||
- Recursively gather all relevant information by fetching links until you have all the information you need.
|
- Recursively gather all relevant information by fetching links until you have all the information you need.
|
||||||
|
|
||||||
## 5. Develop a Detailed Plan
|
## 5. Develop a Detailed Plan
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ The messages you send before tool calls should describe what is immediately abou
|
|||||||
|
|
||||||
Your final message should read naturally, like an update from a concise teammate. For casual conversation, brainstorming tasks, or quick questions from the user, respond in a friendly, conversational tone. You should ask questions, suggest ideas, and adapt to the user’s style. If you've finished a large amount of work, when describing what you've done to the user, you should follow the final answer formatting guidelines to communicate substantive changes. You don't need to add structured formatting for one-word answers, greetings, or purely conversational exchanges.
|
Your final message should read naturally, like an update from a concise teammate. For casual conversation, brainstorming tasks, or quick questions from the user, respond in a friendly, conversational tone. You should ask questions, suggest ideas, and adapt to the user’s style. If you've finished a large amount of work, when describing what you've done to the user, you should follow the final answer formatting guidelines to communicate substantive changes. You don't need to add structured formatting for one-word answers, greetings, or purely conversational exchanges.
|
||||||
|
|
||||||
You can skip heavy formatting for single, simple actions or confirmations. In these cases, respond in plain sentences with any relevant next step or quick option. Reserve multi-section structured responses for results that need grouping or explanation.
|
You can skip heavy formatting for single, simple actions or confirmations. In these cases, respond in plain sentences with any relevant next step or quick option. Reserve multisection structured responses for results that need grouping or explanation.
|
||||||
|
|
||||||
The user is working on the same computer as you, and has access to your work. As such there's no need to show the full contents of large files you have already written unless the user explicitly asks for them. Similarly, if you've created or modified files using `edit`, there's no need to tell users to "save the file" or "copy the code into a file"—just reference the file path.
|
The user is working on the same computer as you, and has access to your work. As such there's no need to show the full contents of large files you have already written unless the user explicitly asks for them. Similarly, if you've created or modified files using `edit`, there's no need to tell users to "save the file" or "copy the code into a file"—just reference the file path.
|
||||||
|
|
||||||
@@ -259,7 +259,7 @@ You are producing plain text that will later be styled by the CLI. Follow these
|
|||||||
**File References**
|
**File References**
|
||||||
When referencing files in your response, make sure to include the relevant start line and always follow the below rules:
|
When referencing files in your response, make sure to include the relevant start line and always follow the below rules:
|
||||||
* Use inline code to make file paths clickable.
|
* Use inline code to make file paths clickable.
|
||||||
* Each reference should have a stand alone path. Even if it's the same file.
|
* Each reference should have a standalone path. Even if it's the same file.
|
||||||
* Accepted: absolute, workspace‑relative, a/ or b/ diff prefixes, or bare filename/suffix.
|
* Accepted: absolute, workspace‑relative, a/ or b/ diff prefixes, or bare filename/suffix.
|
||||||
* Line/column (1‑based, optional): :line[:column] or #Lline[Ccolumn] (column defaults to 1).
|
* Line/column (1‑based, optional): :line[:column] or #Lline[Ccolumn] (column defaults to 1).
|
||||||
* Do not use URIs like file://, vscode://, or https://.
|
* Do not use URIs like file://, vscode://, or https://.
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { Ripgrep } from "../file/ripgrep"
|
|||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import { Filesystem } from "../util/filesystem"
|
import { Filesystem } from "../util/filesystem"
|
||||||
import { Config } from "../config/config"
|
import { Config } from "../config/config"
|
||||||
import { Skill } from "../skill"
|
|
||||||
|
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|||||||
@@ -32,44 +32,59 @@ export namespace Skill {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const SKILL_GLOB = new Bun.Glob("skill/**/SKILL.md")
|
const OPENCODE_SKILL_GLOB = new Bun.Glob("skill/**/SKILL.md")
|
||||||
|
const CLAUDE_SKILL_GLOB = new Bun.Glob(".claude/skills/**/SKILL.md")
|
||||||
|
|
||||||
export const state = Instance.state(async () => {
|
export const state = Instance.state(async () => {
|
||||||
const directories = await Config.directories()
|
const directories = await Config.directories()
|
||||||
const skills: Record<string, Info> = {}
|
const skills: Record<string, Info> = {}
|
||||||
|
|
||||||
|
const addSkill = async (match: string) => {
|
||||||
|
const md = await ConfigMarkdown.parse(match)
|
||||||
|
if (!md) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Info.pick({ name: true, description: true }).safeParse(md.data)
|
||||||
|
if (!parsed.success) return
|
||||||
|
|
||||||
|
// Warn on duplicate skill names
|
||||||
|
if (skills[parsed.data.name]) {
|
||||||
|
log.warn("duplicate skill name", {
|
||||||
|
name: parsed.data.name,
|
||||||
|
existing: skills[parsed.data.name].location,
|
||||||
|
duplicate: match,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
skills[parsed.data.name] = {
|
||||||
|
name: parsed.data.name,
|
||||||
|
description: parsed.data.description,
|
||||||
|
location: match,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const dir of directories) {
|
for (const dir of directories) {
|
||||||
for await (const match of SKILL_GLOB.scan({
|
for await (const match of OPENCODE_SKILL_GLOB.scan({
|
||||||
cwd: dir,
|
cwd: dir,
|
||||||
absolute: true,
|
absolute: true,
|
||||||
onlyFiles: true,
|
onlyFiles: true,
|
||||||
followSymlinks: true,
|
followSymlinks: true,
|
||||||
})) {
|
})) {
|
||||||
const md = await ConfigMarkdown.parse(match)
|
await addSkill(match)
|
||||||
if (!md) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = Info.pick({ name: true, description: true }).safeParse(md.data)
|
|
||||||
if (!parsed.success) continue
|
|
||||||
|
|
||||||
// Warn on duplicate skill names
|
|
||||||
if (skills[parsed.data.name]) {
|
|
||||||
log.warn("duplicate skill name", {
|
|
||||||
name: parsed.data.name,
|
|
||||||
existing: skills[parsed.data.name].location,
|
|
||||||
duplicate: match,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
skills[parsed.data.name] = {
|
|
||||||
name: parsed.data.name,
|
|
||||||
description: parsed.data.description,
|
|
||||||
location: match,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for await (const match of CLAUDE_SKILL_GLOB.scan({
|
||||||
|
cwd: Instance.worktree,
|
||||||
|
absolute: true,
|
||||||
|
onlyFiles: true,
|
||||||
|
followSymlinks: true,
|
||||||
|
dot: true,
|
||||||
|
})) {
|
||||||
|
await addSkill(match)
|
||||||
|
}
|
||||||
|
|
||||||
return skills
|
return skills
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
|
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
|
||||||
- Returns matching file paths sorted by modification time
|
- Returns matching file paths sorted by modification time
|
||||||
- Use this tool when you need to find files by name patterns
|
- Use this tool when you need to find files by name patterns
|
||||||
- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Task tool instead
|
- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead
|
||||||
- You have the capability to call multiple tools in a single response. It is always better to speculatively perform multiple searches as a batch that are potentially useful.
|
- You have the capability to call multiple tools in a single response. It is always better to speculatively perform multiple searches as a batch that are potentially useful.
|
||||||
|
|||||||
@@ -26,20 +26,25 @@ export const SkillTool: Tool.Info<typeof parameters> = {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const description =
|
||||||
|
accessibleSkills.length === 0
|
||||||
|
? "Load a skill to get detailed instructions for a specific task. No skills are currently available."
|
||||||
|
: [
|
||||||
|
"Load a skill to get detailed instructions for a specific task.",
|
||||||
|
"Skills provide specialized knowledge and step-by-step guidance.",
|
||||||
|
"Use this when a task matches an available skill's description.",
|
||||||
|
"<available_skills>",
|
||||||
|
...accessibleSkills.flatMap((skill) => [
|
||||||
|
` <skill>`,
|
||||||
|
` <name>${skill.name}</name>`,
|
||||||
|
` <description>${skill.description}</description>`,
|
||||||
|
` </skill>`,
|
||||||
|
]),
|
||||||
|
"</available_skills>",
|
||||||
|
].join(" ")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
description: [
|
description,
|
||||||
"Load a skill to get detailed instructions for a specific task.",
|
|
||||||
"Skills provide specialized knowledge and step-by-step guidance.",
|
|
||||||
"Use this when a task matches an available skill's description.",
|
|
||||||
"<available_skills>",
|
|
||||||
...accessibleSkills.flatMap((skill) => [
|
|
||||||
` <skill>`,
|
|
||||||
` <name>${skill.name}</name>`,
|
|
||||||
` <description>${skill.description}</description>`,
|
|
||||||
` </skill>`,
|
|
||||||
]),
|
|
||||||
"</available_skills>",
|
|
||||||
].join(" "),
|
|
||||||
parameters,
|
parameters,
|
||||||
async execute(params, ctx) {
|
async execute(params, ctx) {
|
||||||
const agent = await Agent.get(ctx.agent)
|
const agent = await Agent.get(ctx.agent)
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ export const WebFetchTool = Tool.define("webfetch", {
|
|||||||
url: z.string().describe("The URL to fetch content from"),
|
url: z.string().describe("The URL to fetch content from"),
|
||||||
format: z
|
format: z
|
||||||
.enum(["text", "markdown", "html"])
|
.enum(["text", "markdown", "html"])
|
||||||
.describe("The format to return the content in (text, markdown, or html)"),
|
.default("markdown")
|
||||||
|
.describe("The format to return the content in (text, markdown, or html). Defaults to markdown."),
|
||||||
timeout: z.number().describe("Optional timeout in seconds (max 120)").optional(),
|
timeout: z.number().describe("Optional timeout in seconds (max 120)").optional(),
|
||||||
}),
|
}),
|
||||||
async execute(params, ctx) {
|
async execute(params, ctx) {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
- Fetches content from a specified URL
|
- Fetches content from a specified URL
|
||||||
- Takes a URL and a prompt as input
|
- Takes a URL and optional format as input
|
||||||
- Fetches the URL content, converts HTML to markdown
|
- Fetches the URL content, converts to requested format (markdown by default)
|
||||||
- Returns the model's response about the content
|
- Returns the content in the specified format
|
||||||
- Use this tool when you need to retrieve and analyze web content
|
- Use this tool when you need to retrieve and analyze web content
|
||||||
|
|
||||||
Usage notes:
|
Usage notes:
|
||||||
- IMPORTANT: if another tool is present that offers better web fetching capabilities, is more targeted to the task, or has fewer restrictions, prefer using that tool instead of this one.
|
- IMPORTANT: if another tool is present that offers better web fetching capabilities, is more targeted to the task, or has fewer restrictions, prefer using that tool instead of this one.
|
||||||
- The URL must be a fully-formed valid URL
|
- The URL must be a fully-formed valid URL
|
||||||
- HTTP URLs will be automatically upgraded to HTTPS
|
- HTTP URLs will be automatically upgraded to HTTPS
|
||||||
- The prompt should describe what information you want to extract from the page
|
- Format options: "markdown" (default), "text", or "html"
|
||||||
- This tool is read-only and does not modify any files
|
- This tool is read-only and does not modify any files
|
||||||
- Results may be summarized if the content is very large
|
- Results may be summarized if the content is very large
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { test, expect, describe } from "bun:test"
|
||||||
|
import { extractResponseText } from "../../src/cli/cmd/github"
|
||||||
|
import type { MessageV2 } from "../../src/session/message-v2"
|
||||||
|
|
||||||
|
// Helper to create minimal valid parts
|
||||||
|
function createTextPart(text: string): MessageV2.Part {
|
||||||
|
return {
|
||||||
|
id: "1",
|
||||||
|
sessionID: "s",
|
||||||
|
messageID: "m",
|
||||||
|
type: "text" as const,
|
||||||
|
text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createReasoningPart(text: string): MessageV2.Part {
|
||||||
|
return {
|
||||||
|
id: "1",
|
||||||
|
sessionID: "s",
|
||||||
|
messageID: "m",
|
||||||
|
type: "reasoning" as const,
|
||||||
|
text,
|
||||||
|
time: { start: 0 },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createToolPart(tool: string, title: string, status: "completed" | "running" = "completed"): MessageV2.Part {
|
||||||
|
if (status === "completed") {
|
||||||
|
return {
|
||||||
|
id: "1",
|
||||||
|
sessionID: "s",
|
||||||
|
messageID: "m",
|
||||||
|
type: "tool" as const,
|
||||||
|
callID: "c1",
|
||||||
|
tool,
|
||||||
|
state: {
|
||||||
|
status: "completed",
|
||||||
|
input: {},
|
||||||
|
output: "",
|
||||||
|
title,
|
||||||
|
metadata: {},
|
||||||
|
time: { start: 0, end: 1 },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: "1",
|
||||||
|
sessionID: "s",
|
||||||
|
messageID: "m",
|
||||||
|
type: "tool" as const,
|
||||||
|
callID: "c1",
|
||||||
|
tool,
|
||||||
|
state: {
|
||||||
|
status: "running",
|
||||||
|
input: {},
|
||||||
|
time: { start: 0 },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStepStartPart(): MessageV2.Part {
|
||||||
|
return {
|
||||||
|
id: "1",
|
||||||
|
sessionID: "s",
|
||||||
|
messageID: "m",
|
||||||
|
type: "step-start" as const,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("extractResponseText", () => {
|
||||||
|
test("returns text from text part", () => {
|
||||||
|
const parts = [createTextPart("Hello world")]
|
||||||
|
expect(extractResponseText(parts)).toBe("Hello world")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns last text part when multiple exist", () => {
|
||||||
|
const parts = [createTextPart("First"), createTextPart("Last")]
|
||||||
|
expect(extractResponseText(parts)).toBe("Last")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns text even when tool parts follow", () => {
|
||||||
|
const parts = [createTextPart("I'll help with that."), createToolPart("todowrite", "3 todos")]
|
||||||
|
expect(extractResponseText(parts)).toBe("I'll help with that.")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns null for reasoning-only response (signals summary needed)", () => {
|
||||||
|
const parts = [createReasoningPart("Let me think about this...")]
|
||||||
|
expect(extractResponseText(parts)).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns null for tool-only response (signals summary needed)", () => {
|
||||||
|
// This is the exact scenario from the bug report - todowrite with no text
|
||||||
|
const parts = [createToolPart("todowrite", "8 todos")]
|
||||||
|
expect(extractResponseText(parts)).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns null for multiple completed tools", () => {
|
||||||
|
const parts = [
|
||||||
|
createToolPart("read", "src/file.ts"),
|
||||||
|
createToolPart("edit", "src/file.ts"),
|
||||||
|
createToolPart("bash", "bun test"),
|
||||||
|
]
|
||||||
|
expect(extractResponseText(parts)).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("ignores running tool parts (throws since no completed tools)", () => {
|
||||||
|
const parts = [createToolPart("bash", "", "running")]
|
||||||
|
expect(() => extractResponseText(parts)).toThrow("Failed to parse response")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("throws with part types on empty array", () => {
|
||||||
|
expect(() => extractResponseText([])).toThrow("Part types found: [none]")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("throws with part types on unhandled parts", () => {
|
||||||
|
const parts = [createStepStartPart()]
|
||||||
|
expect(() => extractResponseText(parts)).toThrow("Part types found: [step-start]")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("prefers text over reasoning when both present", () => {
|
||||||
|
const parts = [createReasoningPart("Internal thinking..."), createTextPart("Final answer")]
|
||||||
|
expect(extractResponseText(parts)).toBe("Final answer")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("prefers text over tools when both present", () => {
|
||||||
|
const parts = [createToolPart("read", "src/file.ts"), createTextPart("Here's what I found")]
|
||||||
|
expect(extractResponseText(parts)).toBe("Here's what I found")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -14,7 +14,7 @@ but this is not:
|
|||||||
|
|
||||||
We also need to deal with files followed by @commas, ones
|
We also need to deal with files followed by @commas, ones
|
||||||
with @file-extensions.md, even @multiple.extensions.bak,
|
with @file-extensions.md, even @multiple.extensions.bak,
|
||||||
hidden directorys like @.config/ or files like @.bashrc
|
hidden directories like @.config/ or files like @.bashrc
|
||||||
and ones at the end of a sentence like @foo.md.
|
and ones at the end of a sentence like @foo.md.
|
||||||
|
|
||||||
Also shouldn't forget @/absolute/paths.txt with and @/without/extensions,
|
Also shouldn't forget @/absolute/paths.txt with and @/without/extensions,
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
import { test, expect } from "bun:test"
|
||||||
|
import path from "path"
|
||||||
|
import { tmpdir } from "../fixture/fixture"
|
||||||
|
import { Instance } from "../../src/project/instance"
|
||||||
|
import { Provider } from "../../src/provider/provider"
|
||||||
|
import { Env } from "../../src/env"
|
||||||
|
import { Auth } from "../../src/auth"
|
||||||
|
import { Global } from "../../src/global"
|
||||||
|
|
||||||
|
test("Bedrock: config region takes precedence over AWS_REGION env var", async () => {
|
||||||
|
await using tmp = await tmpdir({
|
||||||
|
init: async (dir) => {
|
||||||
|
await Bun.write(
|
||||||
|
path.join(dir, "opencode.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
$schema: "https://opencode.ai/config.json",
|
||||||
|
provider: {
|
||||||
|
"amazon-bedrock": {
|
||||||
|
options: {
|
||||||
|
region: "eu-west-1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
init: async () => {
|
||||||
|
Env.set("AWS_REGION", "us-east-1")
|
||||||
|
Env.set("AWS_PROFILE", "default")
|
||||||
|
},
|
||||||
|
fn: async () => {
|
||||||
|
const providers = await Provider.list()
|
||||||
|
expect(providers["amazon-bedrock"]).toBeDefined()
|
||||||
|
// Region from config should be used (not env var)
|
||||||
|
expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-1")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Bedrock: falls back to AWS_REGION env var when no config", async () => {
|
||||||
|
await using tmp = await tmpdir({
|
||||||
|
init: async (dir) => {
|
||||||
|
await Bun.write(
|
||||||
|
path.join(dir, "opencode.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
$schema: "https://opencode.ai/config.json",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
init: async () => {
|
||||||
|
Env.set("AWS_REGION", "eu-west-1")
|
||||||
|
Env.set("AWS_PROFILE", "default")
|
||||||
|
},
|
||||||
|
fn: async () => {
|
||||||
|
const providers = await Provider.list()
|
||||||
|
expect(providers["amazon-bedrock"]).toBeDefined()
|
||||||
|
expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-1")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Bedrock: without explicit region config, uses AWS_REGION env or defaults", async () => {
|
||||||
|
await using tmp = await tmpdir({
|
||||||
|
init: async (dir) => {
|
||||||
|
await Bun.write(
|
||||||
|
path.join(dir, "opencode.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
$schema: "https://opencode.ai/config.json",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
init: async () => {
|
||||||
|
Env.set("AWS_PROFILE", "default")
|
||||||
|
// AWS_REGION might be set in the environment, use that or default
|
||||||
|
},
|
||||||
|
fn: async () => {
|
||||||
|
const providers = await Provider.list()
|
||||||
|
expect(providers["amazon-bedrock"]).toBeDefined()
|
||||||
|
// Should have some region set (either from env or default)
|
||||||
|
expect(providers["amazon-bedrock"].options?.region).toBeDefined()
|
||||||
|
expect(typeof providers["amazon-bedrock"].options?.region).toBe("string")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Bedrock: uses config region in provider options", async () => {
|
||||||
|
await using tmp = await tmpdir({
|
||||||
|
init: async (dir) => {
|
||||||
|
await Bun.write(
|
||||||
|
path.join(dir, "opencode.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
$schema: "https://opencode.ai/config.json",
|
||||||
|
provider: {
|
||||||
|
"amazon-bedrock": {
|
||||||
|
options: {
|
||||||
|
region: "eu-north-1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
init: async () => {
|
||||||
|
Env.set("AWS_PROFILE", "default")
|
||||||
|
},
|
||||||
|
fn: async () => {
|
||||||
|
const providers = await Provider.list()
|
||||||
|
const bedrockProvider = providers["amazon-bedrock"]
|
||||||
|
expect(bedrockProvider).toBeDefined()
|
||||||
|
expect(bedrockProvider.options?.region).toBe("eu-north-1")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Bedrock: respects config region for different instances", async () => {
|
||||||
|
// First instance with EU config
|
||||||
|
await using tmp1 = await tmpdir({
|
||||||
|
init: async (dir) => {
|
||||||
|
await Bun.write(
|
||||||
|
path.join(dir, "opencode.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
$schema: "https://opencode.ai/config.json",
|
||||||
|
provider: {
|
||||||
|
"amazon-bedrock": {
|
||||||
|
options: {
|
||||||
|
region: "eu-west-1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp1.path,
|
||||||
|
init: async () => {
|
||||||
|
Env.set("AWS_PROFILE", "default")
|
||||||
|
Env.set("AWS_REGION", "us-east-1")
|
||||||
|
},
|
||||||
|
fn: async () => {
|
||||||
|
const providers1 = await Provider.list()
|
||||||
|
expect(providers1["amazon-bedrock"].options?.region).toBe("eu-west-1")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Second instance with US config
|
||||||
|
await using tmp2 = await tmpdir({
|
||||||
|
init: async (dir) => {
|
||||||
|
await Bun.write(
|
||||||
|
path.join(dir, "opencode.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
$schema: "https://opencode.ai/config.json",
|
||||||
|
provider: {
|
||||||
|
"amazon-bedrock": {
|
||||||
|
options: {
|
||||||
|
region: "us-west-2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp2.path,
|
||||||
|
init: async () => {
|
||||||
|
Env.set("AWS_PROFILE", "default")
|
||||||
|
Env.set("AWS_REGION", "eu-west-1")
|
||||||
|
},
|
||||||
|
fn: async () => {
|
||||||
|
const providers2 = await Provider.list()
|
||||||
|
expect(providers2["amazon-bedrock"].options?.region).toBe("us-west-2")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Bedrock: loads when bearer token from auth.json is present", async () => {
|
||||||
|
await using tmp = await tmpdir({
|
||||||
|
init: async (dir) => {
|
||||||
|
await Bun.write(
|
||||||
|
path.join(dir, "opencode.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
$schema: "https://opencode.ai/config.json",
|
||||||
|
provider: {
|
||||||
|
"amazon-bedrock": {
|
||||||
|
options: {
|
||||||
|
region: "eu-west-1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Setup auth.json with bearer token for amazon-bedrock
|
||||||
|
const authPath = path.join(Global.Path.data, "auth.json")
|
||||||
|
await Bun.write(
|
||||||
|
authPath,
|
||||||
|
JSON.stringify({
|
||||||
|
"amazon-bedrock": {
|
||||||
|
type: "api",
|
||||||
|
key: "test-bearer-token",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
init: async () => {
|
||||||
|
// Clear env vars so only auth.json should trigger autoload
|
||||||
|
Env.set("AWS_PROFILE", "")
|
||||||
|
Env.set("AWS_ACCESS_KEY_ID", "")
|
||||||
|
Env.set("AWS_BEARER_TOKEN_BEDROCK", "")
|
||||||
|
},
|
||||||
|
fn: async () => {
|
||||||
|
const providers = await Provider.list()
|
||||||
|
expect(providers["amazon-bedrock"]).toBeDefined()
|
||||||
|
expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-1")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -101,31 +101,31 @@ test("returns empty array when no skills exist", async () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// test("discovers skills from .claude/skills/ directory", async () => {
|
test("discovers skills from .claude/skills/ directory", async () => {
|
||||||
// await using tmp = await tmpdir({
|
await using tmp = await tmpdir({
|
||||||
// git: true,
|
git: true,
|
||||||
// init: async (dir) => {
|
init: async (dir) => {
|
||||||
// const skillDir = path.join(dir, ".claude", "skills", "claude-skill")
|
const skillDir = path.join(dir, ".claude", "skills", "claude-skill")
|
||||||
// await Bun.write(
|
await Bun.write(
|
||||||
// path.join(skillDir, "SKILL.md"),
|
path.join(skillDir, "SKILL.md"),
|
||||||
// `---
|
`---
|
||||||
// name: claude-skill
|
name: claude-skill
|
||||||
// description: A skill in the .claude/skills directory.
|
description: A skill in the .claude/skills directory.
|
||||||
// ---
|
---
|
||||||
|
|
||||||
// # Claude Skill
|
# Claude Skill
|
||||||
// `,
|
`,
|
||||||
// )
|
)
|
||||||
// },
|
},
|
||||||
// })
|
})
|
||||||
|
|
||||||
// await Instance.provide({
|
await Instance.provide({
|
||||||
// directory: tmp.path,
|
directory: tmp.path,
|
||||||
// fn: async () => {
|
fn: async () => {
|
||||||
// const skills = await Skill.all()
|
const skills = await Skill.all()
|
||||||
// expect(skills.length).toBe(1)
|
expect(skills.length).toBe(1)
|
||||||
// expect(skills[0].name).toBe("claude-skill")
|
expect(skills[0].name).toBe("claude-skill")
|
||||||
// expect(skills[0].location).toContain(".claude/skills/claude-skill/SKILL.md")
|
expect(skills[0].location).toContain(".claude/skills/claude-skill/SKILL.md")
|
||||||
// },
|
},
|
||||||
// })
|
})
|
||||||
// })
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.0.204",
|
"version": "1.0.209",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo --noEmit",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.0.204",
|
"version": "1.0.209",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo --noEmit",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.0.204",
|
"version": "1.0.209",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run src/index.ts",
|
"dev": "bun run src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.0.204",
|
"version": "1.0.209",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
"./*": "./src/components/*.tsx",
|
"./*": "./src/components/*.tsx",
|
||||||
@@ -11,6 +11,9 @@
|
|||||||
"./context/*": "./src/context/*.tsx",
|
"./context/*": "./src/context/*.tsx",
|
||||||
"./styles": "./src/styles/index.css",
|
"./styles": "./src/styles/index.css",
|
||||||
"./styles/tailwind": "./src/styles/tailwind/index.css",
|
"./styles/tailwind": "./src/styles/tailwind/index.css",
|
||||||
|
"./theme": "./src/theme/index.ts",
|
||||||
|
"./theme/*": "./src/theme/*.ts",
|
||||||
|
"./theme/context": "./src/theme/context.tsx",
|
||||||
"./icons/provider": "./src/components/provider-icons/types.ts",
|
"./icons/provider": "./src/components/provider-icons/types.ts",
|
||||||
"./icons/file-type": "./src/components/file-icons/types.ts",
|
"./icons/file-type": "./src/components/file-icons/types.ts",
|
||||||
"./fonts/*": "./src/assets/fonts/*",
|
"./fonts/*": "./src/assets/fonts/*",
|
||||||
@@ -22,15 +25,16 @@
|
|||||||
"generate:tailwind": "bun run script/tailwind.ts"
|
"generate:tailwind": "bun run script/tailwind.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "catalog:",
|
"@tailwindcss/vite": "catalog:",
|
||||||
"@types/luxon": "catalog:",
|
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
|
"@types/bun": "catalog:",
|
||||||
|
"@types/katex": "0.16.7",
|
||||||
|
"@types/luxon": "catalog:",
|
||||||
|
"tailwindcss": "catalog:",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
"vite": "catalog:",
|
"vite": "catalog:",
|
||||||
"vite-plugin-icons-spritesheet": "3.0.1",
|
"vite-plugin-icons-spritesheet": "3.0.1",
|
||||||
"vite-plugin-solid": "catalog:",
|
"vite-plugin-solid": "catalog:"
|
||||||
"tailwindcss": "catalog:",
|
|
||||||
"@tailwindcss/vite": "catalog:"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
@@ -43,8 +47,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:",
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
[data-component="image-preview"] {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 50;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
[data-slot="image-preview-container"] {
|
||||||
|
position: relative;
|
||||||
|
z-index: 50;
|
||||||
|
width: min(calc(100vw - 32px), 90vw);
|
||||||
|
max-width: 1200px;
|
||||||
|
height: min(calc(100vh - 32px), 90vh);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
[data-slot="image-preview-content"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
background: var(--surface-raised-stronger-non-alpha);
|
||||||
|
box-shadow:
|
||||||
|
0 15px 45px 0 rgba(19, 16, 16, 0.35),
|
||||||
|
0 3.35px 10.051px 0 rgba(19, 16, 16, 0.25),
|
||||||
|
0 0.998px 2.993px 0 rgba(19, 16, 16, 0.2);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="image-preview-header"] {
|
||||||
|
display: flex;
|
||||||
|
padding: 8px 8px 0;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="image-preview-body"] {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="image-preview-image"] {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: calc(90vh - 100px);
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Dialog as Kobalte } from "@kobalte/core/dialog"
|
||||||
|
import { IconButton } from "./icon-button"
|
||||||
|
|
||||||
|
export interface ImagePreviewProps {
|
||||||
|
src: string
|
||||||
|
alt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ImagePreview(props: ImagePreviewProps) {
|
||||||
|
return (
|
||||||
|
<div data-component="image-preview">
|
||||||
|
<div data-slot="image-preview-container">
|
||||||
|
<Kobalte.Content data-slot="image-preview-content">
|
||||||
|
<div data-slot="image-preview-header">
|
||||||
|
<Kobalte.CloseButton data-slot="image-preview-close" as={IconButton} icon="close" variant="ghost" />
|
||||||
|
</div>
|
||||||
|
<div data-slot="image-preview-body">
|
||||||
|
<img src={props.src} alt={props.alt ?? "Image preview"} data-slot="image-preview-image" />
|
||||||
|
</div>
|
||||||
|
</Kobalte.Content>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { type FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks"
|
import { type FilteredListProps, useFilteredList } from "@opencode-ai/ui/hooks"
|
||||||
import { createEffect, createSignal, For, type JSX, on, Show } from "solid-js"
|
import { createEffect, createSignal, For, onCleanup, type JSX, on, Show } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { Icon, type IconProps } from "./icon"
|
import { Icon, type IconProps } from "./icon"
|
||||||
import { IconButton } from "./icon-button"
|
import { IconButton } from "./icon-button"
|
||||||
@@ -15,6 +15,7 @@ export interface ListProps<T> extends FilteredListProps<T> {
|
|||||||
children: (item: T) => JSX.Element
|
children: (item: T) => JSX.Element
|
||||||
emptyMessage?: string
|
emptyMessage?: string
|
||||||
onKeyEvent?: (event: KeyboardEvent, item: T | undefined) => void
|
onKeyEvent?: (event: KeyboardEvent, item: T | undefined) => void
|
||||||
|
onMove?: (item: T | undefined) => void
|
||||||
activeIcon?: IconProps["name"]
|
activeIcon?: IconProps["name"]
|
||||||
filter?: string
|
filter?: string
|
||||||
search?: ListSearchProps | boolean
|
search?: ListSearchProps | boolean
|
||||||
@@ -82,6 +83,13 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|||||||
element?.scrollIntoView({ block: "center", behavior: "smooth" })
|
element?.scrollIntoView({ block: "center", behavior: "smooth" })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const all = flat()
|
||||||
|
const current = active()
|
||||||
|
const item = all.find((x) => props.key(x) === current)
|
||||||
|
props.onMove?.(item)
|
||||||
|
})
|
||||||
|
|
||||||
const handleSelect = (item: T | undefined, index: number) => {
|
const handleSelect = (item: T | undefined, index: number) => {
|
||||||
props.onSelect?.(item, index)
|
props.onSelect?.(item, index)
|
||||||
}
|
}
|
||||||
@@ -108,6 +116,33 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|||||||
setScrollRef,
|
setScrollRef,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function GroupHeader(props: { category: string }): JSX.Element {
|
||||||
|
const [stuck, setStuck] = createSignal(false)
|
||||||
|
const [header, setHeader] = createSignal<HTMLDivElement | undefined>(undefined)
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const scroll = scrollRef()
|
||||||
|
const node = header()
|
||||||
|
if (!scroll || !node) return
|
||||||
|
|
||||||
|
const handler = () => {
|
||||||
|
const rect = node.getBoundingClientRect()
|
||||||
|
const scrollRect = scroll.getBoundingClientRect()
|
||||||
|
setStuck(rect.top <= scrollRect.top + 1 && scroll.scrollTop > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
scroll.addEventListener("scroll", handler, { passive: true })
|
||||||
|
handler()
|
||||||
|
onCleanup(() => scroll.removeEventListener("scroll", handler))
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-slot="list-header" data-stuck={stuck()} ref={setHeader}>
|
||||||
|
{props.category}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-component="list" classList={{ [props.class ?? ""]: !!props.class }}>
|
<div data-component="list" classList={{ [props.class ?? ""]: !!props.class }}>
|
||||||
<Show when={!!props.search}>
|
<Show when={!!props.search}>
|
||||||
@@ -149,31 +184,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
|
|||||||
{(group) => (
|
{(group) => (
|
||||||
<div data-slot="list-group">
|
<div data-slot="list-group">
|
||||||
<Show when={group.category}>
|
<Show when={group.category}>
|
||||||
{(() => {
|
<GroupHeader category={group.category} />
|
||||||
const [stuck, setStuck] = createSignal(false)
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="list-header"
|
|
||||||
data-stuck={stuck()}
|
|
||||||
ref={(el) => {
|
|
||||||
createEffect(() => {
|
|
||||||
const scroll = scrollRef()
|
|
||||||
if (!scroll) return
|
|
||||||
const handler = () => {
|
|
||||||
const rect = el.getBoundingClientRect()
|
|
||||||
const scrollRect = scroll.getBoundingClientRect()
|
|
||||||
setStuck(rect.top <= scrollRect.top + 1 && scroll.scrollTop > 0)
|
|
||||||
}
|
|
||||||
scroll.addEventListener("scroll", handler, { passive: true })
|
|
||||||
handler()
|
|
||||||
return () => scroll.removeEventListener("scroll", handler)
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{group.category}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})()}
|
|
||||||
</Show>
|
</Show>
|
||||||
<div data-slot="list-items">
|
<div data-slot="list-items">
|
||||||
<For each={group.items}>
|
<For each={group.items}>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
color: var(--text-base);
|
color: var(--text-base);
|
||||||
font-family: var(--font-family-sans);
|
font-family: var(--font-family-sans);
|
||||||
font-size: var(--font-size-base); /* 14px */
|
font-size: var(--font-size-base); /* 14px */
|
||||||
line-height: var(--line-height-large);
|
line-height: var(--line-height-x-large);
|
||||||
|
|
||||||
/* Spacing for flow */
|
/* Spacing for flow */
|
||||||
> *:first-child {
|
> *:first-child {
|
||||||
@@ -64,8 +64,16 @@
|
|||||||
list-style-position: inside;
|
list-style-position: inside;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol {
|
||||||
|
list-style-type: decimal;
|
||||||
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
li::marker {
|
li::marker {
|
||||||
@@ -117,13 +125,15 @@
|
|||||||
:not(pre) > code {
|
:not(pre) > code {
|
||||||
font-family: var(--font-family-mono);
|
font-family: var(--font-family-mono);
|
||||||
font-feature-settings: var(--font-family-mono--font-feature-settings);
|
font-feature-settings: var(--font-family-mono--font-feature-settings);
|
||||||
font-size: 13px;
|
color: var(--syntax-string);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
/* font-size: 13px; */
|
||||||
|
|
||||||
padding: 2px 2px;
|
/* padding: 2px 2px; */
|
||||||
margin: 0 1.5px;
|
/* margin: 0 1.5px; */
|
||||||
border-radius: 2px;
|
/* border-radius: 2px; */
|
||||||
background: var(--surface-base);
|
/* background: var(--surface-base); */
|
||||||
box-shadow: 0 0 0 0.5px var(--border-weak-base);
|
/* box-shadow: 0 0 0 0.5px var(--border-weak-base); */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tables */
|
/* Tables */
|
||||||
|
|||||||
@@ -40,6 +40,10 @@
|
|||||||
border-color: var(--border-strong-base);
|
border-color: var(--border-strong-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&[data-clickable="true"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
&[data-type="image"] {
|
&[data-type="image"] {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
@@ -137,6 +141,9 @@
|
|||||||
|
|
||||||
[data-slot="message-part-tool-error-message"] {
|
[data-slot="message-part-tool-error-message"] {
|
||||||
color: var(--text-on-critical-weak);
|
color: var(--text-on-critical-weak);
|
||||||
|
max-height: 240px;
|
||||||
|
overflow-y: auto;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,6 +249,14 @@
|
|||||||
|
|
||||||
[data-component="edit-content"] {
|
[data-component="edit-content"] {
|
||||||
border-top: 1px solid var(--border-weaker-base);
|
border-top: 1px solid var(--border-weaker-base);
|
||||||
|
max-height: 420px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="write-content"] {
|
[data-component="write-content"] {
|
||||||
@@ -367,9 +382,9 @@
|
|||||||
|
|
||||||
&[data-permission="true"] {
|
&[data-permission="true"] {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: var(--sticky-header-height, 80px);
|
top: calc(2px + var(--sticky-header-height, 40px));
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
z-index: 10;
|
z-index: 20;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: var(--shadow-xs-border-base);
|
box-shadow: var(--shadow-xs-border-base);
|
||||||
@@ -380,6 +395,7 @@
|
|||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: -1.5px;
|
inset: -1.5px;
|
||||||
|
top: -5px;
|
||||||
border-radius: 7.5px;
|
border-radius: 7.5px;
|
||||||
border: 1.5px solid transparent;
|
border: 1.5px solid transparent;
|
||||||
background:
|
background:
|
||||||
@@ -387,12 +403,12 @@
|
|||||||
conic-gradient(
|
conic-gradient(
|
||||||
from var(--border-angle),
|
from var(--border-angle),
|
||||||
transparent 0deg,
|
transparent 0deg,
|
||||||
transparent 270deg,
|
transparent 0deg,
|
||||||
var(--border-warning-strong, var(--border-warning-selected)) 300deg,
|
var(--border-warning-strong, var(--border-warning-selected)) 300deg,
|
||||||
var(--border-warning-base) 360deg
|
var(--border-warning-base) 360deg
|
||||||
)
|
)
|
||||||
border-box;
|
border-box;
|
||||||
animation: chase-border 1.5s linear infinite;
|
animation: chase-border 2.5s linear infinite;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,15 @@
|
|||||||
import { Component, createEffect, createMemo, createSignal, For, Match, Show, Switch, type JSX } from "solid-js"
|
import {
|
||||||
|
Component,
|
||||||
|
createEffect,
|
||||||
|
createMemo,
|
||||||
|
createSignal,
|
||||||
|
For,
|
||||||
|
Match,
|
||||||
|
Show,
|
||||||
|
Switch,
|
||||||
|
onCleanup,
|
||||||
|
type JSX,
|
||||||
|
} from "solid-js"
|
||||||
import { Dynamic } from "solid-js/web"
|
import { Dynamic } from "solid-js/web"
|
||||||
import {
|
import {
|
||||||
AssistantMessage,
|
AssistantMessage,
|
||||||
@@ -14,6 +25,7 @@ import {
|
|||||||
import { useData } from "../context"
|
import { useData } from "../context"
|
||||||
import { useDiffComponent } from "../context/diff"
|
import { useDiffComponent } from "../context/diff"
|
||||||
import { useCodeComponent } from "../context/code"
|
import { useCodeComponent } from "../context/code"
|
||||||
|
import { useDialog } from "../context/dialog"
|
||||||
import { BasicTool } from "./basic-tool"
|
import { BasicTool } from "./basic-tool"
|
||||||
import { GenericTool } from "./basic-tool"
|
import { GenericTool } from "./basic-tool"
|
||||||
import { Button } from "./button"
|
import { Button } from "./button"
|
||||||
@@ -22,6 +34,7 @@ import { Icon } from "./icon"
|
|||||||
import { Checkbox } from "./checkbox"
|
import { Checkbox } from "./checkbox"
|
||||||
import { DiffChanges } from "./diff-changes"
|
import { DiffChanges } from "./diff-changes"
|
||||||
import { Markdown } from "./markdown"
|
import { Markdown } from "./markdown"
|
||||||
|
import { ImagePreview } from "./image-preview"
|
||||||
import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/util/path"
|
import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||||
import { checksum } from "@opencode-ai/util/encode"
|
import { checksum } from "@opencode-ai/util/encode"
|
||||||
import { createAutoScroll } from "../hooks"
|
import { createAutoScroll } from "../hooks"
|
||||||
@@ -80,6 +93,47 @@ export type PartComponent = Component<MessagePartProps>
|
|||||||
|
|
||||||
export const PART_MAPPING: Record<string, PartComponent | undefined> = {}
|
export const PART_MAPPING: Record<string, PartComponent | undefined> = {}
|
||||||
|
|
||||||
|
const TEXT_RENDER_THROTTLE_MS = 100
|
||||||
|
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
|
||||||
|
function createThrottledValue(getValue: () => string) {
|
||||||
|
const [value, setValue] = createSignal(getValue())
|
||||||
|
let timeout: ReturnType<typeof setTimeout> | undefined
|
||||||
|
let last = 0
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const next = getValue()
|
||||||
|
const now = Date.now()
|
||||||
|
const remaining = TEXT_RENDER_THROTTLE_MS - (now - last)
|
||||||
|
if (remaining <= 0) {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
timeout = undefined
|
||||||
|
}
|
||||||
|
last = now
|
||||||
|
setValue(next)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (timeout) clearTimeout(timeout)
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
last = Date.now()
|
||||||
|
setValue(next)
|
||||||
|
timeout = undefined
|
||||||
|
}, remaining)
|
||||||
|
})
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
if (timeout) clearTimeout(timeout)
|
||||||
|
})
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
function relativizeProjectPaths(text: string, directory?: string) {
|
function relativizeProjectPaths(text: string, directory?: string) {
|
||||||
if (!text) return ""
|
if (!text) return ""
|
||||||
if (!directory) return text
|
if (!directory) return text
|
||||||
@@ -209,15 +263,21 @@ export function Message(props: MessageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function AssistantMessageDisplay(props: { message: AssistantMessage; parts: PartType[] }) {
|
export function AssistantMessageDisplay(props: { message: AssistantMessage; parts: PartType[] }) {
|
||||||
const filteredParts = createMemo(() => {
|
const emptyParts: PartType[] = []
|
||||||
return props.parts?.filter((x) => {
|
const filteredParts = createMemo(
|
||||||
return x.type !== "tool" || (x as ToolPart).tool !== "todoread"
|
() =>
|
||||||
})
|
props.parts.filter((x) => {
|
||||||
})
|
return x.type !== "tool" || (x as ToolPart).tool !== "todoread"
|
||||||
|
}),
|
||||||
|
emptyParts,
|
||||||
|
{ equals: same },
|
||||||
|
)
|
||||||
return <For each={filteredParts()}>{(part) => <Part part={part} message={props.message} />}</For>
|
return <For each={filteredParts()}>{(part) => <Part part={part} message={props.message} />}</For>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[] }) {
|
export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[] }) {
|
||||||
|
const dialog = useDialog()
|
||||||
|
|
||||||
const textPart = createMemo(
|
const textPart = createMemo(
|
||||||
() => props.parts?.find((p) => p.type === "text" && !(p as TextPart).synthetic) as TextPart | undefined,
|
() => props.parts?.find((p) => p.type === "text" && !(p as TextPart).synthetic) as TextPart | undefined,
|
||||||
)
|
)
|
||||||
@@ -240,13 +300,26 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const openImagePreview = (url: string, alt?: string) => {
|
||||||
|
dialog.show(() => <ImagePreview src={url} alt={alt} />)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-component="user-message">
|
<div data-component="user-message">
|
||||||
<Show when={attachments().length > 0}>
|
<Show when={attachments().length > 0}>
|
||||||
<div data-slot="user-message-attachments">
|
<div data-slot="user-message-attachments">
|
||||||
<For each={attachments()}>
|
<For each={attachments()}>
|
||||||
{(file) => (
|
{(file) => (
|
||||||
<div data-slot="user-message-attachment" data-type={file.mime.startsWith("image/") ? "image" : "file"}>
|
<div
|
||||||
|
data-slot="user-message-attachment"
|
||||||
|
data-type={file.mime.startsWith("image/") ? "image" : "file"}
|
||||||
|
data-clickable={file.mime.startsWith("image/") && !!file.url}
|
||||||
|
onClick={() => {
|
||||||
|
if (file.mime.startsWith("image/") && file.url) {
|
||||||
|
openImagePreview(file.url, file.filename)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Show
|
<Show
|
||||||
when={file.mime.startsWith("image/") && file.url}
|
when={file.mime.startsWith("image/") && file.url}
|
||||||
fallback={
|
fallback={
|
||||||
@@ -362,9 +435,22 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
|||||||
const part = props.part as ToolPart
|
const part = props.part as ToolPart
|
||||||
|
|
||||||
const permission = createMemo(() => {
|
const permission = createMemo(() => {
|
||||||
const sessionID = props.message.sessionID
|
const next = data.store.permission?.[props.message.sessionID]?.[0]
|
||||||
const permissions = data.store.permission?.[sessionID] ?? []
|
if (!next) return undefined
|
||||||
return permissions.find((p) => p.callID === part.callID)
|
if (next.callID !== part.callID) return undefined
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
|
||||||
|
const [showPermission, setShowPermission] = createSignal(false)
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const perm = permission()
|
||||||
|
if (perm) {
|
||||||
|
const timeout = setTimeout(() => setShowPermission(true), 50)
|
||||||
|
onCleanup(() => clearTimeout(timeout))
|
||||||
|
} else {
|
||||||
|
setShowPermission(false)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const [forceOpen, setForceOpen] = createSignal(false)
|
const [forceOpen, setForceOpen] = createSignal(false)
|
||||||
@@ -382,13 +468,17 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const component = createMemo(() => {
|
const emptyInput: Record<string, any> = {}
|
||||||
const render = ToolRegistry.render(part.tool) ?? GenericTool
|
const emptyMetadata: Record<string, any> = {}
|
||||||
// @ts-expect-error
|
|
||||||
const metadata = part.state?.metadata ?? {}
|
|
||||||
const input = part.state?.input ?? {}
|
|
||||||
|
|
||||||
return (
|
const input = () => part.state?.input ?? emptyInput
|
||||||
|
// @ts-expect-error
|
||||||
|
const metadata = () => part.state?.metadata ?? emptyMetadata
|
||||||
|
|
||||||
|
const render = ToolRegistry.render(part.tool) ?? GenericTool
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-component="tool-part-wrapper" data-permission={showPermission()}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={part.state.status === "error" && part.state.error}>
|
<Match when={part.state.status === "error" && part.state.error}>
|
||||||
{(error) => {
|
{(error) => {
|
||||||
@@ -417,9 +507,9 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
|||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
<Dynamic
|
<Dynamic
|
||||||
component={render}
|
component={render}
|
||||||
input={input}
|
input={input()}
|
||||||
tool={part.tool}
|
tool={part.tool}
|
||||||
metadata={metadata}
|
metadata={metadata()}
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
output={part.state.output}
|
output={part.state.output}
|
||||||
status={part.state.status}
|
status={part.state.status}
|
||||||
@@ -429,13 +519,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
|||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
)
|
<Show when={showPermission() && permission()}>
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div data-component="tool-part-wrapper" data-permission={!!permission()}>
|
|
||||||
<Show when={component()}>{component()}</Show>
|
|
||||||
<Show when={permission()}>
|
|
||||||
{(perm) => (
|
{(perm) => (
|
||||||
<div data-component="permission-prompt">
|
<div data-component="permission-prompt">
|
||||||
<div data-slot="permission-message">{perm().title}</div>
|
<div data-slot="permission-message">{perm().title}</div>
|
||||||
@@ -461,11 +545,12 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
|
|||||||
const data = useData()
|
const data = useData()
|
||||||
const part = props.part as TextPart
|
const part = props.part as TextPart
|
||||||
const displayText = () => relativizeProjectPaths((part.text ?? "").trim(), data.directory)
|
const displayText = () => relativizeProjectPaths((part.text ?? "").trim(), data.directory)
|
||||||
|
const throttledText = createThrottledValue(displayText)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={displayText()}>
|
<Show when={throttledText()}>
|
||||||
<div data-component="text-part">
|
<div data-component="text-part">
|
||||||
<Markdown text={displayText()} />
|
<Markdown text={throttledText()} />
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
@@ -473,10 +558,13 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
|
|||||||
|
|
||||||
PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props) {
|
PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props) {
|
||||||
const part = props.part as ReasoningPart
|
const part = props.part as ReasoningPart
|
||||||
|
const text = () => part.text.trim()
|
||||||
|
const throttledText = createThrottledValue(text)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={part.text.trim()}>
|
<Show when={throttledText()}>
|
||||||
<div data-component="reasoning-part">
|
<div data-component="reasoning-part">
|
||||||
<Markdown text={part.text.trim()} />
|
<Markdown text={throttledText()} />
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
@@ -623,7 +711,7 @@ ToolRegistry.register({
|
|||||||
const sessionId = childSessionId()
|
const sessionId = childSessionId()
|
||||||
if (!sessionId) return undefined
|
if (!sessionId) return undefined
|
||||||
const permissions = data.store.permission?.[sessionId] ?? []
|
const permissions = data.store.permission?.[sessionId] ?? []
|
||||||
return permissions.toSorted((a, b) => a.id.localeCompare(b.id))[0]
|
return permissions[0]
|
||||||
})
|
})
|
||||||
|
|
||||||
const childToolPart = createMemo(() => {
|
const childToolPart = createMemo(() => {
|
||||||
@@ -807,19 +895,19 @@ ToolRegistry.register({
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Show when={props.metadata.filediff}>
|
<Show when={props.metadata.filediff?.path || props.input.filePath}>
|
||||||
<div data-component="edit-content">
|
<div data-component="edit-content">
|
||||||
<Dynamic
|
<Dynamic
|
||||||
component={diffComponent}
|
component={diffComponent}
|
||||||
before={{
|
before={{
|
||||||
name: props.metadata.filediff.path,
|
name: props.metadata?.filediff?.file || props.input.filePath,
|
||||||
contents: props.metadata.filediff.before,
|
contents: props.metadata?.filediff?.before || props.input.oldString,
|
||||||
cacheKey: checksum(props.metadata.filediff.before),
|
cacheKey: checksum(props.metadata?.filediff?.before || props.input.oldString),
|
||||||
}}
|
}}
|
||||||
after={{
|
after={{
|
||||||
name: props.metadata.filediff.path,
|
name: props.metadata?.filediff?.file || props.input.filePath,
|
||||||
contents: props.metadata.filediff.after,
|
contents: props.metadata?.filediff?.after || props.input.newString,
|
||||||
cacheKey: checksum(props.metadata.filediff.after),
|
cacheKey: checksum(props.metadata?.filediff?.after || props.input.newString),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -343,6 +343,8 @@
|
|||||||
|
|
||||||
.error-card {
|
.error-card {
|
||||||
color: var(--text-on-critical-base);
|
color: var(--text-on-critical-base);
|
||||||
|
max-height: 240px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="session-turn-collapsible-content-inner"] {
|
[data-slot="session-turn-collapsible-content-inner"] {
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
import { AssistantMessage, Part as PartType, TextPart, ToolPart } from "@opencode-ai/sdk/v2/client"
|
import {
|
||||||
|
AssistantMessage,
|
||||||
|
Message as MessageType,
|
||||||
|
Part as PartType,
|
||||||
|
type Permission,
|
||||||
|
TextPart,
|
||||||
|
ToolPart,
|
||||||
|
} from "@opencode-ai/sdk/v2/client"
|
||||||
import { useData } from "../context"
|
import { useData } from "../context"
|
||||||
import { useDiffComponent } from "../context/diff"
|
import { useDiffComponent } from "../context/diff"
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||||
import { checksum } from "@opencode-ai/util/encode"
|
import { checksum } from "@opencode-ai/util/encode"
|
||||||
import { createEffect, createMemo, For, Match, onCleanup, ParentProps, Show, Switch } from "solid-js"
|
import { Binary } from "@opencode-ai/util/binary"
|
||||||
|
import { createEffect, createMemo, For, Match, on, onCleanup, ParentProps, Show, Switch } from "solid-js"
|
||||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||||
import { DiffChanges } from "./diff-changes"
|
import { DiffChanges } from "./diff-changes"
|
||||||
import { Typewriter } from "./typewriter"
|
import { Typewriter } from "./typewriter"
|
||||||
@@ -60,26 +68,38 @@ function computeStatusFromPart(part: PartType | undefined): string | undefined {
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
|
||||||
function AssistantMessageItem(props: {
|
function AssistantMessageItem(props: {
|
||||||
message: AssistantMessage
|
message: AssistantMessage
|
||||||
summary: string | undefined
|
responsePartId: string | undefined
|
||||||
response: string | undefined
|
hideResponsePart: boolean
|
||||||
lastTextPartId: string | undefined
|
|
||||||
working: boolean
|
|
||||||
}) {
|
}) {
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const msgParts = createMemo(() => data.store.part[props.message.id] ?? [])
|
const emptyParts: PartType[] = []
|
||||||
const lastTextPart = createMemo(() =>
|
const msgParts = createMemo(() => data.store.part[props.message.id] ?? emptyParts)
|
||||||
msgParts()
|
const lastTextPart = createMemo(() => {
|
||||||
.filter((p) => p?.type === "text")
|
const parts = msgParts()
|
||||||
.at(-1),
|
for (let i = parts.length - 1; i >= 0; i--) {
|
||||||
)
|
const part = parts[i]
|
||||||
|
if (part?.type === "text") return part as TextPart
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
|
||||||
const filteredParts = createMemo(() => {
|
const filteredParts = createMemo(() => {
|
||||||
if (!props.working && !props.summary && props.response && props.lastTextPartId === lastTextPart()?.id) {
|
const parts = msgParts()
|
||||||
return msgParts().filter((p) => p?.id !== lastTextPart()?.id)
|
if (!props.hideResponsePart) return parts
|
||||||
}
|
|
||||||
return msgParts()
|
const responsePartId = props.responsePartId
|
||||||
|
if (!responsePartId) return parts
|
||||||
|
if (responsePartId !== lastTextPart()?.id) return parts
|
||||||
|
|
||||||
|
return parts.filter((part) => part?.id !== responsePartId)
|
||||||
})
|
})
|
||||||
|
|
||||||
return <Message message={props.message} parts={filteredParts()} />
|
return <Message message={props.message} parts={filteredParts()} />
|
||||||
@@ -89,6 +109,7 @@ export function SessionTurn(
|
|||||||
props: ParentProps<{
|
props: ParentProps<{
|
||||||
sessionID: string
|
sessionID: string
|
||||||
messageID: string
|
messageID: string
|
||||||
|
lastUserMessageID?: string
|
||||||
stepsExpanded?: boolean
|
stepsExpanded?: boolean
|
||||||
onStepsExpandedToggle?: () => void
|
onStepsExpandedToggle?: () => void
|
||||||
onUserInteracted?: () => void
|
onUserInteracted?: () => void
|
||||||
@@ -102,27 +123,76 @@ export function SessionTurn(
|
|||||||
const data = useData()
|
const data = useData()
|
||||||
const diffComponent = useDiffComponent()
|
const diffComponent = useDiffComponent()
|
||||||
|
|
||||||
const allMessages = createMemo(() => data.store.message[props.sessionID] ?? [])
|
const emptyMessages: MessageType[] = []
|
||||||
const userMessages = createMemo(() =>
|
const emptyParts: PartType[] = []
|
||||||
allMessages()
|
const emptyAssistant: AssistantMessage[] = []
|
||||||
.filter((m) => m.role === "user")
|
const emptyPermissions: Permission[] = []
|
||||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
const emptyPermissionParts: { part: ToolPart; message: AssistantMessage }[] = []
|
||||||
)
|
const idle = { type: "idle" as const }
|
||||||
|
|
||||||
const message = createMemo(() => userMessages().find((m) => m.id === props.messageID))
|
const allMessages = createMemo(() => data.store.message[props.sessionID] ?? emptyMessages)
|
||||||
const isLastUserMessage = createMemo(() => message()?.id === userMessages().at(-1)?.id)
|
|
||||||
|
const messageIndex = createMemo(() => {
|
||||||
|
const messages = allMessages()
|
||||||
|
const result = Binary.search(messages, props.messageID, (m) => m.id)
|
||||||
|
if (!result.found) return -1
|
||||||
|
|
||||||
|
const msg = messages[result.index]
|
||||||
|
if (msg.role !== "user") return -1
|
||||||
|
|
||||||
|
return result.index
|
||||||
|
})
|
||||||
|
|
||||||
|
const message = createMemo(() => {
|
||||||
|
const index = messageIndex()
|
||||||
|
if (index < 0) return undefined
|
||||||
|
|
||||||
|
const msg = allMessages()[index]
|
||||||
|
if (!msg || msg.role !== "user") return undefined
|
||||||
|
|
||||||
|
return msg
|
||||||
|
})
|
||||||
|
|
||||||
|
const lastUserMessageID = createMemo(() => {
|
||||||
|
if (props.lastUserMessageID) return props.lastUserMessageID
|
||||||
|
|
||||||
|
const messages = allMessages()
|
||||||
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
|
const msg = messages[i]
|
||||||
|
if (msg?.role === "user") return msg.id
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const isLastUserMessage = createMemo(() => props.messageID === lastUserMessageID())
|
||||||
|
|
||||||
const parts = createMemo(() => {
|
const parts = createMemo(() => {
|
||||||
const msg = message()
|
const msg = message()
|
||||||
if (!msg) return []
|
if (!msg) return emptyParts
|
||||||
return data.store.part[msg.id] ?? []
|
return data.store.part[msg.id] ?? emptyParts
|
||||||
})
|
})
|
||||||
|
|
||||||
const assistantMessages = createMemo(() => {
|
const assistantMessages = createMemo(
|
||||||
const msg = message()
|
() => {
|
||||||
if (!msg) return [] as AssistantMessage[]
|
const msg = message()
|
||||||
return allMessages().filter((m) => m.role === "assistant" && m.parentID === msg.id) as AssistantMessage[]
|
if (!msg) return emptyAssistant
|
||||||
})
|
|
||||||
|
const messages = allMessages()
|
||||||
|
const index = messageIndex()
|
||||||
|
if (index < 0) return emptyAssistant
|
||||||
|
|
||||||
|
const result: AssistantMessage[] = []
|
||||||
|
for (let i = index + 1; i < messages.length; i++) {
|
||||||
|
const item = messages[i]
|
||||||
|
if (!item) continue
|
||||||
|
if (item.role === "user") break
|
||||||
|
if (item.role === "assistant" && item.parentID === msg.id) result.push(item as AssistantMessage)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
emptyAssistant,
|
||||||
|
{ equals: same },
|
||||||
|
)
|
||||||
|
|
||||||
const lastAssistantMessage = createMemo(() => assistantMessages().at(-1))
|
const lastAssistantMessage = createMemo(() => assistantMessages().at(-1))
|
||||||
|
|
||||||
@@ -131,7 +201,7 @@ export function SessionTurn(
|
|||||||
const lastTextPart = createMemo(() => {
|
const lastTextPart = createMemo(() => {
|
||||||
const msgs = assistantMessages()
|
const msgs = assistantMessages()
|
||||||
for (let mi = msgs.length - 1; mi >= 0; mi--) {
|
for (let mi = msgs.length - 1; mi >= 0; mi--) {
|
||||||
const msgParts = data.store.part[msgs[mi].id] ?? []
|
const msgParts = data.store.part[msgs[mi].id] ?? emptyParts
|
||||||
for (let pi = msgParts.length - 1; pi >= 0; pi--) {
|
for (let pi = msgParts.length - 1; pi >= 0; pi--) {
|
||||||
const part = msgParts[pi]
|
const part = msgParts[pi]
|
||||||
if (part?.type === "text") return part as TextPart
|
if (part?.type === "text") return part as TextPart
|
||||||
@@ -151,20 +221,25 @@ export function SessionTurn(
|
|||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
const permissionParts = createMemo(() => {
|
const permissions = createMemo(() => data.store.permission?.[props.sessionID] ?? emptyPermissions)
|
||||||
const result: { part: ToolPart; message: AssistantMessage }[] = []
|
const permissionCount = createMemo(() => permissions().length)
|
||||||
const permissions = data.store.permission?.[props.sessionID] ?? []
|
const nextPermission = createMemo(() => permissions()[0])
|
||||||
if (!permissions.length) return result
|
|
||||||
|
|
||||||
for (const m of assistantMessages()) {
|
const permissionParts = createMemo(() => {
|
||||||
const msgParts = data.store.part[m.id] ?? []
|
if (props.stepsExpanded) return emptyPermissionParts
|
||||||
for (const p of msgParts) {
|
|
||||||
if (p?.type === "tool" && permissions.some((perm) => perm.callID === (p as ToolPart).callID)) {
|
const next = nextPermission()
|
||||||
result.push({ part: p as ToolPart, message: m })
|
if (!next) return emptyPermissionParts
|
||||||
}
|
|
||||||
|
for (const message of assistantMessages()) {
|
||||||
|
const parts = data.store.part[message.id] ?? emptyParts
|
||||||
|
for (const part of parts) {
|
||||||
|
if (part?.type !== "tool") continue
|
||||||
|
const tool = part as ToolPart
|
||||||
|
if (tool.callID === next.callID) return [{ part: tool, message }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return emptyPermissionParts
|
||||||
})
|
})
|
||||||
|
|
||||||
const shellModePart = createMemo(() => {
|
const shellModePart = createMemo(() => {
|
||||||
@@ -174,7 +249,7 @@ export function SessionTurn(
|
|||||||
const msgs = assistantMessages()
|
const msgs = assistantMessages()
|
||||||
if (msgs.length !== 1) return
|
if (msgs.length !== 1) return
|
||||||
|
|
||||||
const msgParts = data.store.part[msgs[0].id] ?? []
|
const msgParts = data.store.part[msgs[0].id] ?? emptyParts
|
||||||
if (msgParts.length !== 1) return
|
if (msgParts.length !== 1) return
|
||||||
|
|
||||||
const assistantPart = msgParts[0]
|
const assistantPart = msgParts[0]
|
||||||
@@ -189,7 +264,7 @@ export function SessionTurn(
|
|||||||
let currentTask: ToolPart | undefined
|
let currentTask: ToolPart | undefined
|
||||||
|
|
||||||
for (let mi = msgs.length - 1; mi >= 0; mi--) {
|
for (let mi = msgs.length - 1; mi >= 0; mi--) {
|
||||||
const msgParts = data.store.part[msgs[mi].id] ?? []
|
const msgParts = data.store.part[msgs[mi].id] ?? emptyParts
|
||||||
for (let pi = msgParts.length - 1; pi >= 0; pi--) {
|
for (let pi = msgParts.length - 1; pi >= 0; pi--) {
|
||||||
const part = msgParts[pi]
|
const part = msgParts[pi]
|
||||||
if (!part) continue
|
if (!part) continue
|
||||||
@@ -216,12 +291,12 @@ export function SessionTurn(
|
|||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
if (taskSessionId) {
|
if (taskSessionId) {
|
||||||
const taskMessages = data.store.message[taskSessionId] ?? []
|
const taskMessages = data.store.message[taskSessionId] ?? emptyMessages
|
||||||
for (let mi = taskMessages.length - 1; mi >= 0; mi--) {
|
for (let mi = taskMessages.length - 1; mi >= 0; mi--) {
|
||||||
const msg = taskMessages[mi]
|
const msg = taskMessages[mi]
|
||||||
if (!msg || msg.role !== "assistant") continue
|
if (!msg || msg.role !== "assistant") continue
|
||||||
|
|
||||||
const msgParts = data.store.part[msg.id] ?? []
|
const msgParts = data.store.part[msg.id] ?? emptyParts
|
||||||
for (let pi = msgParts.length - 1; pi >= 0; pi--) {
|
for (let pi = msgParts.length - 1; pi >= 0; pi--) {
|
||||||
const part = msgParts[pi]
|
const part = msgParts[pi]
|
||||||
if (part) return computeStatusFromPart(part)
|
if (part) return computeStatusFromPart(part)
|
||||||
@@ -232,12 +307,7 @@ export function SessionTurn(
|
|||||||
return computeStatusFromPart(last)
|
return computeStatusFromPart(last)
|
||||||
})
|
})
|
||||||
|
|
||||||
const status = createMemo(
|
const status = createMemo(() => data.store.session_status[props.sessionID] ?? idle)
|
||||||
() =>
|
|
||||||
data.store.session_status[props.sessionID] ?? {
|
|
||||||
type: "idle",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
const working = createMemo(() => status().type !== "idle" && isLastUserMessage())
|
const working = createMemo(() => status().type !== "idle" && isLastUserMessage())
|
||||||
const retry = createMemo(() => {
|
const retry = createMemo(() => {
|
||||||
const s = status()
|
const s = status()
|
||||||
@@ -245,12 +315,11 @@ export function SessionTurn(
|
|||||||
return s
|
return s
|
||||||
})
|
})
|
||||||
|
|
||||||
const summary = () => message()?.summary?.body
|
const summary = createMemo(() => message()?.summary?.body)
|
||||||
const response = () => {
|
const response = createMemo(() => lastTextPart()?.text)
|
||||||
const part = lastTextPart()
|
const responsePartId = createMemo(() => lastTextPart()?.id)
|
||||||
return part?.type === "text" ? (part as TextPart).text : undefined
|
const hasDiffs = createMemo(() => message()?.summary?.diffs?.length)
|
||||||
}
|
const hideResponsePart = createMemo(() => !working() && !summary() && !!responsePartId())
|
||||||
const hasDiffs = () => message()?.summary?.diffs?.length
|
|
||||||
|
|
||||||
function duration() {
|
function duration() {
|
||||||
const msg = message()
|
const msg = message()
|
||||||
@@ -328,6 +397,14 @@ export function SessionTurn(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(permissionCount, (count, prev) => {
|
||||||
|
if (!count) return
|
||||||
|
if (prev !== undefined && count <= prev) return
|
||||||
|
autoScroll.forceScrollToBottom()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (working() || !isLastUserMessage()) return
|
if (working() || !isLastUserMessage()) return
|
||||||
|
|
||||||
@@ -336,6 +413,12 @@ export function SessionTurn(
|
|||||||
if (summary()) return
|
if (summary()) return
|
||||||
if (store.summaryWaitTimedOut) return
|
if (store.summaryWaitTimedOut) return
|
||||||
|
|
||||||
|
const completed = lastAssistantMessage()?.time.completed
|
||||||
|
if (completed && Date.now() - completed > 2000) {
|
||||||
|
setStore("summaryWaitTimedOut", true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setStore("summaryWaitTimedOut", true)
|
setStore("summaryWaitTimedOut", true)
|
||||||
}, 6000)
|
}, 6000)
|
||||||
@@ -471,10 +554,8 @@ export function SessionTurn(
|
|||||||
{(assistantMessage) => (
|
{(assistantMessage) => (
|
||||||
<AssistantMessageItem
|
<AssistantMessageItem
|
||||||
message={assistantMessage}
|
message={assistantMessage}
|
||||||
summary={summary()}
|
responsePartId={responsePartId()}
|
||||||
response={response()}
|
hideResponsePart={hideResponsePart()}
|
||||||
lastTextPartId={lastTextPart()?.id}
|
|
||||||
working={working()}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|||||||
@@ -205,4 +205,70 @@
|
|||||||
/* [data-slot="tabs-content"] { */
|
/* [data-slot="tabs-content"] { */
|
||||||
/* } */
|
/* } */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&[data-orientation="vertical"] {
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
[data-slot="tabs-list"] {
|
||||||
|
flex-direction: column;
|
||||||
|
width: auto;
|
||||||
|
height: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
flex-grow: 1;
|
||||||
|
border-bottom: none;
|
||||||
|
border-right: 1px solid var(--border-weak-base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="tabs-trigger-wrapper"] {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-bottom: none;
|
||||||
|
border-right: 1px solid var(--border-weak-base);
|
||||||
|
|
||||||
|
&:has([data-selected]) {
|
||||||
|
border-right-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="tabs-content"] {
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-variant="alt"] {
|
||||||
|
[data-slot="tabs-list"] {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
padding-top: 24px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
border-bottom: none;
|
||||||
|
border-right: 1px solid var(--border-weak-base);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="tabs-trigger-wrapper"] {
|
||||||
|
border-bottom: none;
|
||||||
|
border-right-width: 2px;
|
||||||
|
border-right-style: solid;
|
||||||
|
border-right-color: transparent;
|
||||||
|
|
||||||
|
[data-slot="tabs-trigger"] {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has([data-selected]) {
|
||||||
|
border-right-color: var(--icon-strong-base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { ComponentProps, ParentProps } from "solid-js"
|
|||||||
|
|
||||||
export interface TabsProps extends ComponentProps<typeof Kobalte> {
|
export interface TabsProps extends ComponentProps<typeof Kobalte> {
|
||||||
variant?: "normal" | "alt"
|
variant?: "normal" | "alt"
|
||||||
|
orientation?: "horizontal" | "vertical"
|
||||||
}
|
}
|
||||||
export interface TabsListProps extends ComponentProps<typeof Kobalte.List> {}
|
export interface TabsListProps extends ComponentProps<typeof Kobalte.List> {}
|
||||||
export interface TabsTriggerProps extends ComponentProps<typeof Kobalte.Trigger> {
|
export interface TabsTriggerProps extends ComponentProps<typeof Kobalte.Trigger> {
|
||||||
@@ -16,12 +17,14 @@ export interface TabsTriggerProps extends ComponentProps<typeof Kobalte.Trigger>
|
|||||||
export interface TabsContentProps extends ComponentProps<typeof Kobalte.Content> {}
|
export interface TabsContentProps extends ComponentProps<typeof Kobalte.Content> {}
|
||||||
|
|
||||||
function TabsRoot(props: TabsProps) {
|
function TabsRoot(props: TabsProps) {
|
||||||
const [split, rest] = splitProps(props, ["class", "classList", "variant"])
|
const [split, rest] = splitProps(props, ["class", "classList", "variant", "orientation"])
|
||||||
return (
|
return (
|
||||||
<Kobalte
|
<Kobalte
|
||||||
{...rest}
|
{...rest}
|
||||||
|
orientation={split.orientation}
|
||||||
data-component="tabs"
|
data-component="tabs"
|
||||||
data-variant={split.variant || "normal"}
|
data-variant={split.variant || "normal"}
|
||||||
|
data-orientation={split.orientation || "horizontal"}
|
||||||
classList={{
|
classList={{
|
||||||
...(split.classList ?? {}),
|
...(split.classList ?? {}),
|
||||||
[split.class ?? ""]: !!split.class,
|
[split.class ?? ""]: !!split.class,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[data-component="toast-region"] {
|
[data-component="toast-region"] {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 32px;
|
bottom: 48px;
|
||||||
right: 32px;
|
right: 32px;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ export type ToastVariant = "default" | "success" | "error" | "loading"
|
|||||||
export interface ToastAction {
|
export interface ToastAction {
|
||||||
label: string
|
label: string
|
||||||
onClick: "dismiss" | (() => void)
|
onClick: "dismiss" | (() => void)
|
||||||
dismissAfter?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ToastOptions {
|
export interface ToastOptions {
|
||||||
@@ -132,10 +131,8 @@ export function showToast(options: ToastOptions | string) {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (typeof action.onClick === "function") {
|
if (typeof action.onClick === "function") {
|
||||||
action.onClick()
|
action.onClick()
|
||||||
if (action.dismissAfter) toaster.dismiss(props.toastId)
|
|
||||||
} else {
|
|
||||||
toaster.dismiss(props.toastId)
|
|
||||||
}
|
}
|
||||||
|
toaster.dismiss(props.toastId)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{action.label}
|
{action.label}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { marked } from "marked"
|
import { marked } from "marked"
|
||||||
|
import markedKatex from "marked-katex-extension"
|
||||||
import markedShiki from "marked-shiki"
|
import markedShiki from "marked-shiki"
|
||||||
import { bundledLanguages, type BundledLanguage } from "shiki"
|
import { bundledLanguages, type BundledLanguage } from "shiki"
|
||||||
import { createSimpleContext } from "./helper"
|
import { createSimpleContext } from "./helper"
|
||||||
@@ -378,6 +379,9 @@ export const { use: useMarked, provider: MarkedProvider } = createSimpleContext(
|
|||||||
name: "Marked",
|
name: "Marked",
|
||||||
init: () => {
|
init: () => {
|
||||||
return marked.use(
|
return marked.use(
|
||||||
|
markedKatex({
|
||||||
|
throwOnError: false,
|
||||||
|
}),
|
||||||
markedShiki({
|
markedShiki({
|
||||||
async highlight(code, lang) {
|
async highlight(code, lang) {
|
||||||
const highlighter = await getSharedHighlighter({ themes: ["OpenCode"], langs: [] })
|
const highlighter = await getSharedHighlighter({ themes: ["OpenCode"], langs: [] })
|
||||||
|
|||||||
@@ -19,22 +19,88 @@ export function createAutoScroll(options: AutoScrollOptions) {
|
|||||||
let autoScrollTimeout: ReturnType<typeof setTimeout> | undefined
|
let autoScrollTimeout: ReturnType<typeof setTimeout> | undefined
|
||||||
let isMouseDown = false
|
let isMouseDown = false
|
||||||
let cleanupListeners: (() => void) | undefined
|
let cleanupListeners: (() => void) | undefined
|
||||||
|
let scheduledScroll = false
|
||||||
|
let scheduledForce = false
|
||||||
|
|
||||||
function scrollToBottom() {
|
function distanceFromBottom() {
|
||||||
if (!scrollRef || store.userScrolled || !options.working()) return
|
if (!scrollRef) return 0
|
||||||
|
return scrollRef.scrollHeight - scrollRef.clientHeight - scrollRef.scrollTop
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAutoScroll() {
|
||||||
isAutoScrolling = true
|
isAutoScrolling = true
|
||||||
if (autoScrollTimeout) clearTimeout(autoScrollTimeout)
|
if (autoScrollTimeout) clearTimeout(autoScrollTimeout)
|
||||||
autoScrollTimeout = setTimeout(() => {
|
autoScrollTimeout = setTimeout(() => {
|
||||||
isAutoScrolling = false
|
isAutoScrolling = false
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToBottomNow() {
|
||||||
|
if (!scrollRef || store.userScrolled || !options.working()) return
|
||||||
|
|
||||||
|
const distance = distanceFromBottom()
|
||||||
|
if (distance < 2) return
|
||||||
|
|
||||||
|
const behavior = distance > 96 ? "auto" : "smooth"
|
||||||
|
startAutoScroll()
|
||||||
scrollRef.scrollTo({
|
scrollRef.scrollTo({
|
||||||
top: scrollRef.scrollHeight,
|
top: scrollRef.scrollHeight,
|
||||||
behavior: "smooth",
|
behavior,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function forceScrollToBottomNow() {
|
||||||
|
if (!scrollRef) return
|
||||||
|
|
||||||
|
if (store.userScrolled) setStore("userScrolled", false)
|
||||||
|
|
||||||
|
const distance = distanceFromBottom()
|
||||||
|
if (distance < 2) return
|
||||||
|
|
||||||
|
startAutoScroll()
|
||||||
|
scrollRef.scrollTo({
|
||||||
|
top: scrollRef.scrollHeight,
|
||||||
|
behavior: "auto",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleScrollToBottom(force = false) {
|
||||||
|
if (typeof requestAnimationFrame === "undefined") {
|
||||||
|
if (force) {
|
||||||
|
forceScrollToBottomNow()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
scrollToBottomNow()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (force) scheduledForce = true
|
||||||
|
if (scheduledScroll) return
|
||||||
|
|
||||||
|
scheduledScroll = true
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
scheduledScroll = false
|
||||||
|
|
||||||
|
const shouldForce = scheduledForce
|
||||||
|
scheduledForce = false
|
||||||
|
|
||||||
|
if (shouldForce) {
|
||||||
|
forceScrollToBottomNow()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToBottomNow()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToBottom() {
|
||||||
|
scheduleScrollToBottom(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function forceScrollToBottom() {
|
||||||
|
scheduleScrollToBottom(true)
|
||||||
|
}
|
||||||
|
|
||||||
function handleScroll() {
|
function handleScroll() {
|
||||||
if (!scrollRef) return
|
if (!scrollRef) return
|
||||||
|
|
||||||
@@ -116,6 +182,20 @@ export function createAutoScroll(options: AutoScrollOptions) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Ensure pinned-to-bottom stays pinned during heavy DOM updates
|
||||||
|
createEffect(() => {
|
||||||
|
const el = store.contentRef
|
||||||
|
if (!el) return
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
if (store.userScrolled) return
|
||||||
|
if (!options.working()) return
|
||||||
|
scheduleScrollToBottom(false)
|
||||||
|
})
|
||||||
|
observer.observe(el, { childList: true, subtree: true, characterData: true })
|
||||||
|
onCleanup(() => observer.disconnect())
|
||||||
|
})
|
||||||
|
|
||||||
// Handle content resize
|
// Handle content resize
|
||||||
createResizeObserver(
|
createResizeObserver(
|
||||||
() => store.contentRef,
|
() => store.contentRef,
|
||||||
@@ -161,6 +241,7 @@ export function createAutoScroll(options: AutoScrollOptions) {
|
|||||||
handleScroll,
|
handleScroll,
|
||||||
handleInteraction,
|
handleInteraction,
|
||||||
scrollToBottom,
|
scrollToBottom,
|
||||||
|
forceScrollToBottom,
|
||||||
userScrolled: () => store.userScrolled,
|
userScrolled: () => store.userScrolled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,9 +51,17 @@ export function useFilteredList<T>(props: FilteredListProps<T>) {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function initialActive() {
|
||||||
|
if (props.current) return props.key(props.current)
|
||||||
|
|
||||||
|
const items = flat()
|
||||||
|
if (items.length === 0) return ""
|
||||||
|
return props.key(items[0])
|
||||||
|
}
|
||||||
|
|
||||||
const list = createList({
|
const list = createList({
|
||||||
items: () => flat().map(props.key),
|
items: () => flat().map(props.key),
|
||||||
initialActive: props.current ? props.key(props.current) : props.key(flat()[0]),
|
initialActive: initialActive(),
|
||||||
loop: true,
|
loop: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
@import "./theme.css" layer(theme);
|
@import "./theme.css" layer(theme);
|
||||||
|
|
||||||
@import "./base.css" layer(base);
|
@import "./base.css" layer(base);
|
||||||
|
@import "katex/dist/katex.min.css" layer(base);
|
||||||
|
|
||||||
@import "../components/accordion.css" layer(components);
|
@import "../components/accordion.css" layer(components);
|
||||||
@import "../components/avatar.css" layer(components);
|
@import "../components/avatar.css" layer(components);
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
@import "../components/provider-icon.css" layer(components);
|
@import "../components/provider-icon.css" layer(components);
|
||||||
@import "../components/icon.css" layer(components);
|
@import "../components/icon.css" layer(components);
|
||||||
@import "../components/icon-button.css" layer(components);
|
@import "../components/icon-button.css" layer(components);
|
||||||
|
@import "../components/image-preview.css" layer(components);
|
||||||
@import "../components/text-field.css" layer(components);
|
@import "../components/text-field.css" layer(components);
|
||||||
@import "../components/list.css" layer(components);
|
@import "../components/list.css" layer(components);
|
||||||
@import "../components/logo.css" layer(components);
|
@import "../components/logo.css" layer(components);
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
--text-mix-blend-mode: multiply;
|
--text-mix-blend-mode: multiply;
|
||||||
|
|
||||||
/* OC-1-light */
|
/* OC-1 fallback variables (light) */
|
||||||
--background-base: #f8f7f7;
|
--background-base: #f8f7f7;
|
||||||
--background-weak: var(--smoke-light-3);
|
--background-weak: var(--smoke-light-3);
|
||||||
--background-strong: var(--smoke-light-1);
|
--background-strong: var(--smoke-light-1);
|
||||||
@@ -324,7 +324,7 @@
|
|||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
--text-mix-blend-mode: plus-lighter;
|
--text-mix-blend-mode: plus-lighter;
|
||||||
|
|
||||||
/* OC-1-dark */
|
/* OC-1 fallback variables (dark) */
|
||||||
--background-base: var(--smoke-dark-1);
|
--background-base: var(--smoke-dark-1);
|
||||||
--background-weak: #1c1717;
|
--background-weak: #1c1717;
|
||||||
--background-strong: #151313;
|
--background-strong: #151313;
|
||||||
@@ -577,478 +577,3 @@
|
|||||||
--avatar-text-lime: #c4f042;
|
--avatar-text-lime: #c4f042;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-theme="oc-2-paper"] {
|
|
||||||
/* OC-2-paper */
|
|
||||||
--background-base: #f7f8f8;
|
|
||||||
--background-weak: var(--ink-light-3);
|
|
||||||
--background-strong: var(--ink-light-1);
|
|
||||||
--background-stronger: #fcfcfc;
|
|
||||||
--surface-base: var(--ink-light-alpha-2);
|
|
||||||
--base: var(--ink-light-alpha-2);
|
|
||||||
--surface-base-hover: #0005050f;
|
|
||||||
--surface-base-active: var(--ink-light-alpha-3);
|
|
||||||
--surface-base-interactive-active: var(--cobalt-light-alpha-3);
|
|
||||||
--base2: var(--ink-light-alpha-2);
|
|
||||||
--base3: var(--ink-light-alpha-2);
|
|
||||||
--surface-inset-base: var(--ink-light-alpha-2);
|
|
||||||
--surface-inset-base-hover: var(--ink-light-alpha-3);
|
|
||||||
--surface-inset-strong: #001f1f17;
|
|
||||||
--surface-inset-strong-hover: #001f1f17;
|
|
||||||
--surface-raised-base: var(--ink-light-alpha-1);
|
|
||||||
--surface-float-base: var(--ink-dark-1);
|
|
||||||
--surface-float-base-hover: var(--ink-dark-2);
|
|
||||||
--surface-raised-base-hover: var(--ink-light-alpha-2);
|
|
||||||
--surface-raised-base-active: var(--ink-light-alpha-3);
|
|
||||||
--surface-raised-strong: var(--ink-light-1);
|
|
||||||
--surface-raised-strong-hover: var(--white);
|
|
||||||
--surface-raised-stronger: var(--white);
|
|
||||||
--surface-raised-stronger-hover: var(--white);
|
|
||||||
--surface-weak: var(--ink-light-alpha-3);
|
|
||||||
--surface-weaker: var(--ink-light-alpha-4);
|
|
||||||
--surface-strong: #ffffff;
|
|
||||||
--surface-raised-stronger-non-alpha: var(--white);
|
|
||||||
--surface-brand-base: var(--yuzu-light-9);
|
|
||||||
--surface-brand-hover: var(--yuzu-light-10);
|
|
||||||
--surface-interactive-base: var(--cobalt-light-3);
|
|
||||||
--surface-interactive-hover: var(--cobalt-light-4);
|
|
||||||
--surface-interactive-weak: var(--cobalt-light-2);
|
|
||||||
--surface-interactive-weak-hover: var(--cobalt-light-3);
|
|
||||||
--surface-success-base: var(--apple-light-3);
|
|
||||||
--surface-success-weak: var(--apple-light-2);
|
|
||||||
--surface-success-strong: var(--apple-light-9);
|
|
||||||
--surface-warning-base: var(--solaris-light-3);
|
|
||||||
--surface-warning-weak: var(--solaris-light-2);
|
|
||||||
--surface-warning-strong: var(--solaris-light-9);
|
|
||||||
--surface-critical-base: var(--ember-light-3);
|
|
||||||
--surface-critical-weak: var(--ember-light-2);
|
|
||||||
--surface-critical-strong: var(--ember-light-9);
|
|
||||||
--surface-info-base: var(--lilac-light-3);
|
|
||||||
--surface-info-weak: var(--lilac-light-2);
|
|
||||||
--surface-info-strong: var(--lilac-light-9);
|
|
||||||
--surface-diff-unchanged-base: #ffffff00;
|
|
||||||
--surface-diff-skip-base: var(--ink-light-2);
|
|
||||||
--surface-diff-hidden-base: var(--blue-light-3);
|
|
||||||
--surface-diff-hidden-weak: var(--blue-light-2);
|
|
||||||
--surface-diff-hidden-weaker: var(--blue-light-1);
|
|
||||||
--surface-diff-hidden-strong: var(--blue-light-5);
|
|
||||||
--surface-diff-hidden-stronger: var(--blue-light-9);
|
|
||||||
--surface-diff-add-base: var(--mint-light-3);
|
|
||||||
--surface-diff-add-weak: var(--mint-light-2);
|
|
||||||
--surface-diff-add-weaker: var(--mint-light-1);
|
|
||||||
--surface-diff-add-strong: var(--mint-light-5);
|
|
||||||
--surface-diff-add-stronger: var(--mint-light-9);
|
|
||||||
--surface-diff-delete-base: var(--ember-light-3);
|
|
||||||
--surface-diff-delete-weak: var(--ember-light-2);
|
|
||||||
--surface-diff-delete-weaker: var(--ember-light-1);
|
|
||||||
--surface-diff-delete-strong: var(--ember-light-6);
|
|
||||||
--surface-diff-delete-stronger: var(--ember-light-9);
|
|
||||||
--text-base: var(--ink-light-11);
|
|
||||||
--input-base: var(--ink-light-1);
|
|
||||||
--input-hover: var(--ink-light-2);
|
|
||||||
--input-active: var(--cobalt-light-1);
|
|
||||||
--input-selected: var(--cobalt-light-4);
|
|
||||||
--input-focus: var(--cobalt-light-1);
|
|
||||||
--input-disabled: var(--ink-light-4);
|
|
||||||
--text-weak: var(--ink-light-9);
|
|
||||||
--text-weaker: var(--ink-light-8);
|
|
||||||
--text-strong: var(--ink-light-12);
|
|
||||||
--text-interactive-base: var(--cobalt-light-9);
|
|
||||||
--text-on-brand-base: var(--ink-light-alpha-11);
|
|
||||||
--text-on-interactive-base: var(--ink-light-1);
|
|
||||||
--text-on-interactive-weak: var(--ink-light-alpha-11);
|
|
||||||
--text-on-success-base: var(--apple-light-10);
|
|
||||||
--text-on-critical-base: var(--ember-light-10);
|
|
||||||
--text-on-critical-weak: var(--ember-light-8);
|
|
||||||
--text-on-critical-strong: var(--ember-light-12);
|
|
||||||
--text-on-warning-base: var(--ink-dark-alpha-11);
|
|
||||||
--text-on-info-base: var(--ink-dark-alpha-11);
|
|
||||||
--text-diff-add-base: var(--mint-light-11);
|
|
||||||
--text-diff-delete-base: var(--ember-light-10);
|
|
||||||
--text-diff-delete-strong: var(--ember-light-12);
|
|
||||||
--text-diff-add-strong: var(--mint-light-12);
|
|
||||||
--text-on-info-weak: var(--ink-dark-alpha-9);
|
|
||||||
--text-on-info-strong: var(--ink-dark-alpha-12);
|
|
||||||
--text-on-warning-weak: var(--ink-dark-alpha-9);
|
|
||||||
--text-on-warning-strong: var(--ink-dark-alpha-12);
|
|
||||||
--text-on-success-weak: var(--apple-light-6);
|
|
||||||
--text-on-success-strong: var(--apple-light-12);
|
|
||||||
--text-on-brand-weak: var(--ink-light-alpha-9);
|
|
||||||
--text-on-brand-weaker: var(--ink-light-alpha-8);
|
|
||||||
--text-on-brand-strong: var(--ink-light-alpha-12);
|
|
||||||
--button-secondary-base: #fcfdfd;
|
|
||||||
--button-secondary-hover: #f9fafa;
|
|
||||||
--border-base: var(--ink-light-alpha-7);
|
|
||||||
--border-hover: var(--ink-light-alpha-8);
|
|
||||||
--border-active: var(--ink-light-alpha-9);
|
|
||||||
--border-selected: var(--cobalt-light-alpha-9);
|
|
||||||
--border-disabled: var(--ink-light-alpha-8);
|
|
||||||
--border-focus: var(--ink-light-alpha-9);
|
|
||||||
--border-weak-base: var(--ink-light-alpha-5);
|
|
||||||
--border-strong-base: var(--ink-light-alpha-7);
|
|
||||||
--border-strong-hover: var(--ink-light-alpha-8);
|
|
||||||
--border-strong-active: var(--ink-light-alpha-7);
|
|
||||||
--border-strong-selected: var(--cobalt-light-alpha-6);
|
|
||||||
--border-strong-disabled: var(--ink-light-alpha-6);
|
|
||||||
--border-strong-focus: var(--ink-light-alpha-7);
|
|
||||||
--border-weak-hover: var(--ink-light-alpha-6);
|
|
||||||
--border-weak-active: var(--ink-light-alpha-7);
|
|
||||||
--border-weak-selected: var(--cobalt-light-alpha-5);
|
|
||||||
--border-weak-disabled: var(--ink-light-alpha-6);
|
|
||||||
--border-weak-focus: var(--ink-light-alpha-7);
|
|
||||||
--border-interactive-base: var(--cobalt-light-7);
|
|
||||||
--border-interactive-hover: var(--cobalt-light-8);
|
|
||||||
--border-interactive-active: var(--cobalt-light-9);
|
|
||||||
--border-interactive-selected: var(--cobalt-light-9);
|
|
||||||
--border-interactive-disabled: var(--ink-light-8);
|
|
||||||
--border-interactive-focus: var(--cobalt-light-9);
|
|
||||||
--border-success-base: var(--apple-light-6);
|
|
||||||
--border-success-hover: var(--apple-light-7);
|
|
||||||
--border-success-selected: var(--apple-light-9);
|
|
||||||
--border-warning-base: var(--solaris-light-6);
|
|
||||||
--border-warning-hover: var(--solaris-light-7);
|
|
||||||
--border-warning-selected: var(--solaris-light-9);
|
|
||||||
--border-critical-base: var(--ember-light-6);
|
|
||||||
--border-critical-hover: var(--ember-light-7);
|
|
||||||
--border-critical-selected: var(--ember-light-9);
|
|
||||||
--border-info-base: var(--lilac-light-6);
|
|
||||||
--border-info-hover: var(--lilac-light-7);
|
|
||||||
--border-info-selected: var(--lilac-light-9);
|
|
||||||
--icon-base: var(--ink-light-9);
|
|
||||||
--icon-hover: var(--ink-light-11);
|
|
||||||
--icon-active: var(--ink-light-12);
|
|
||||||
--icon-selected: var(--ink-light-12);
|
|
||||||
--icon-disabled: var(--ink-light-8);
|
|
||||||
--icon-focus: var(--ink-light-12);
|
|
||||||
--icon-invert-base: #ffffff;
|
|
||||||
--icon-weak-base: var(--ink-light-7);
|
|
||||||
--icon-weak-hover: var(--ink-light-8);
|
|
||||||
--icon-weak-active: var(--ink-light-9);
|
|
||||||
--icon-weak-selected: var(--ink-light-10);
|
|
||||||
--icon-weak-disabled: var(--ink-light-6);
|
|
||||||
--icon-weak-focus: var(--ink-light-9);
|
|
||||||
--icon-strong-base: var(--ink-light-12);
|
|
||||||
--icon-strong-hover: #131515;
|
|
||||||
--icon-strong-active: #020202;
|
|
||||||
--icon-strong-selected: #020202;
|
|
||||||
--icon-strong-disabled: var(--ink-light-8);
|
|
||||||
--icon-strong-focus: #020202;
|
|
||||||
--icon-brand-base: var(--ink-light-12);
|
|
||||||
--icon-interactive-base: var(--cobalt-light-9);
|
|
||||||
--icon-success-base: var(--apple-light-7);
|
|
||||||
--icon-success-hover: var(--apple-light-8);
|
|
||||||
--icon-success-active: var(--apple-light-11);
|
|
||||||
--icon-warning-base: var(--amber-light-7);
|
|
||||||
--icon-warning-hover: var(--amber-light-8);
|
|
||||||
--icon-warning-active: var(--amber-light-11);
|
|
||||||
--icon-critical-base: var(--ember-light-10);
|
|
||||||
--icon-critical-hover: var(--ember-light-11);
|
|
||||||
--icon-critical-active: var(--ember-light-12);
|
|
||||||
--icon-info-base: var(--lilac-light-7);
|
|
||||||
--icon-info-hover: var(--lilac-light-8);
|
|
||||||
--icon-info-active: var(--lilac-light-11);
|
|
||||||
--icon-on-brand-base: var(--ink-light-alpha-11);
|
|
||||||
--icon-on-brand-hover: var(--ink-light-alpha-12);
|
|
||||||
--icon-on-brand-selected: var(--ink-light-alpha-12);
|
|
||||||
--icon-on-interactive-base: var(--ink-light-1);
|
|
||||||
--icon-agent-plan-base: var(--purple-light-9);
|
|
||||||
--icon-agent-docs-base: var(--amber-light-9);
|
|
||||||
--icon-agent-ask-base: var(--cyan-light-9);
|
|
||||||
--icon-agent-build-base: var(--cobalt-light-9);
|
|
||||||
--icon-on-success-base: var(--apple-light-alpha-9);
|
|
||||||
--icon-on-success-hover: var(--apple-light-alpha-10);
|
|
||||||
--icon-on-success-selected: var(--apple-light-alpha-11);
|
|
||||||
--icon-on-warning-base: var(--amber-lightalpha-9);
|
|
||||||
--icon-on-warning-hover: var(--amber-lightalpha-10);
|
|
||||||
--icon-on-warning-selected: var(--amber-lightalpha-11);
|
|
||||||
--icon-on-critical-base: var(--ember-light-alpha-9);
|
|
||||||
--icon-on-critical-hover: var(--ember-light-alpha-10);
|
|
||||||
--icon-on-critical-selected: var(--ember-light-alpha-11);
|
|
||||||
--icon-on-info-base: var(--lilac-light-9);
|
|
||||||
--icon-on-info-hover: var(--lilac-light-alpha-10);
|
|
||||||
--icon-on-info-selected: var(--lilac-light-alpha-11);
|
|
||||||
--icon-diff-add-base: var(--mint-light-11);
|
|
||||||
--icon-diff-add-hover: var(--mint-light-12);
|
|
||||||
--icon-diff-add-active: var(--mint-light-12);
|
|
||||||
--icon-diff-delete-base: var(--ember-light-10);
|
|
||||||
--icon-diff-delete-hover: var(--ember-light-11);
|
|
||||||
--syntax-comment: var(--text-weak);
|
|
||||||
--syntax-regexp: var(--text-base);
|
|
||||||
--syntax-string: #007663;
|
|
||||||
--syntax-keyword: var(--text-weak);
|
|
||||||
--syntax-primitive: #fb7f51;
|
|
||||||
--syntax-operator: var(--text-weak);
|
|
||||||
--syntax-variable: var(--text-strong);
|
|
||||||
--syntax-property: #ec6cc8;
|
|
||||||
--syntax-type: #738400;
|
|
||||||
--syntax-constant: #00b2b9;
|
|
||||||
--syntax-punctuation: var(--text-weak);
|
|
||||||
--syntax-object: var(--text-strong);
|
|
||||||
--syntax-success: var(--apple-light-10);
|
|
||||||
--syntax-warning: var(--amber-light-10);
|
|
||||||
--syntax-critical: var(--ember-light-9);
|
|
||||||
--syntax-info: #0091a7;
|
|
||||||
--syntax-diff-add: var(--mint-light-11);
|
|
||||||
--syntax-diff-delete: var(--ember-light-11);
|
|
||||||
--syntax-diff-unknown: #ff0000;
|
|
||||||
--markdown-heading: #d68c27;
|
|
||||||
--markdown-text: #1a1a1a;
|
|
||||||
--markdown-link: #3b7dd8;
|
|
||||||
--markdown-link-text: #318795;
|
|
||||||
--markdown-code: #3d9a57;
|
|
||||||
--markdown-block-quote: #b0851f;
|
|
||||||
--markdown-emph: #b0851f;
|
|
||||||
--markdown-strong: #d68c27;
|
|
||||||
--markdown-horizontal-rule: #8a8a8a;
|
|
||||||
--markdown-list-item: #3b7dd8;
|
|
||||||
--markdown-list-enumeration: #318795;
|
|
||||||
--markdown-image: #3b7dd8;
|
|
||||||
--markdown-image-text: #318795;
|
|
||||||
--markdown-code-block: #1a1a1a;
|
|
||||||
--border-color: #ffffff;
|
|
||||||
--border-weaker-base: var(--ink-light-alpha-3);
|
|
||||||
--border-weaker-hover: var(--ink-light-alpha-4);
|
|
||||||
--border-weaker-active: var(--ink-light-alpha-6);
|
|
||||||
--border-weaker-selected: var(--cobalt-light-alpha-4);
|
|
||||||
--border-weaker-disabled: var(--ink-light-alpha-2);
|
|
||||||
--border-weaker-focus: var(--ink-light-alpha-6);
|
|
||||||
--button-ghost-hover: var(--ink-light-alpha-2);
|
|
||||||
--button-ghost-hover2: var(--ink-light-alpha-3);
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
--background-base: var(--ink-dark-1);
|
|
||||||
--background-weak: #171c1c;
|
|
||||||
--background-strong: #131515;
|
|
||||||
--background-stronger: #151919;
|
|
||||||
--surface-base: var(--ink-dark-alpha-2);
|
|
||||||
--base: var(--ink-dark-alpha-2);
|
|
||||||
--surface-base-hover: #b8e0e00f;
|
|
||||||
--surface-base-active: var(--ink-dark-alpha-3);
|
|
||||||
--surface-base-interactive-active: var(--cobalt-light-alpha-3);
|
|
||||||
--base2: var(--ink-dark-alpha-2);
|
|
||||||
--base3: var(--ink-dark-alpha-2);
|
|
||||||
--surface-inset-base: #0b0e0e7f;
|
|
||||||
--surface-inset-base-hover: #0b0e0e7f;
|
|
||||||
--surface-inset-strong: #050606cc;
|
|
||||||
--surface-inset-strong-hover: #050606cc;
|
|
||||||
--surface-raised-base: var(--ink-light-alpha-1);
|
|
||||||
--surface-float-base: var(--ink-dark-1);
|
|
||||||
--surface-float-base-hover: var(--ink-dark-2);
|
|
||||||
--surface-raised-base-hover: var(--ink-light-alpha-2);
|
|
||||||
--surface-raised-base-active: var(--ink-light-alpha-3);
|
|
||||||
--surface-raised-strong: var(--ink-light-1);
|
|
||||||
--surface-raised-strong-hover: var(--white);
|
|
||||||
--surface-raised-stronger: var(--white);
|
|
||||||
--surface-raised-stronger-hover: var(--white);
|
|
||||||
--surface-weak: var(--ink-dark-alpha-4);
|
|
||||||
--surface-weaker: var(--ink-dark-alpha-5);
|
|
||||||
--surface-strong: var(--ink-dark-alpha-7);
|
|
||||||
--surface-raised-stronger-non-alpha: var(--white);
|
|
||||||
--surface-brand-base: var(--yuzu-light-9);
|
|
||||||
--surface-brand-hover: var(--yuzu-light-10);
|
|
||||||
--surface-interactive-base: var(--cobalt-light-3);
|
|
||||||
--surface-interactive-hover: var(--cobalt-light-4);
|
|
||||||
--surface-interactive-weak: var(--cobalt-light-2);
|
|
||||||
--surface-interactive-weak-hover: var(--cobalt-light-3);
|
|
||||||
--surface-success-base: var(--apple-light-3);
|
|
||||||
--surface-success-weak: var(--apple-light-2);
|
|
||||||
--surface-success-strong: var(--apple-light-9);
|
|
||||||
--surface-warning-base: var(--solaris-light-3);
|
|
||||||
--surface-warning-weak: var(--solaris-light-2);
|
|
||||||
--surface-warning-strong: var(--solaris-light-9);
|
|
||||||
--surface-critical-base: var(--ember-light-3);
|
|
||||||
--surface-critical-weak: var(--ember-light-2);
|
|
||||||
--surface-critical-strong: var(--ember-light-9);
|
|
||||||
--surface-info-base: var(--lilac-light-3);
|
|
||||||
--surface-info-weak: var(--lilac-light-2);
|
|
||||||
--surface-info-strong: var(--lilac-light-9);
|
|
||||||
--surface-diff-unchanged-base: #ffffff00;
|
|
||||||
--surface-diff-skip-base: var(--ink-light-2);
|
|
||||||
--surface-diff-hidden-base: var(--blue-light-3);
|
|
||||||
--surface-diff-hidden-weak: var(--blue-light-2);
|
|
||||||
--surface-diff-hidden-weaker: var(--blue-light-1);
|
|
||||||
--surface-diff-hidden-strong: var(--blue-light-5);
|
|
||||||
--surface-diff-hidden-stronger: var(--blue-light-9);
|
|
||||||
--surface-diff-add-base: var(--mint-light-3);
|
|
||||||
--surface-diff-add-weak: var(--mint-light-2);
|
|
||||||
--surface-diff-add-weaker: var(--mint-light-1);
|
|
||||||
--surface-diff-add-strong: var(--mint-light-5);
|
|
||||||
--surface-diff-add-stronger: var(--mint-light-9);
|
|
||||||
--surface-diff-delete-base: var(--ember-light-3);
|
|
||||||
--surface-diff-delete-weak: var(--ember-light-2);
|
|
||||||
--surface-diff-delete-weaker: var(--ember-light-1);
|
|
||||||
--surface-diff-delete-strong: var(--ember-light-6);
|
|
||||||
--surface-diff-delete-stronger: var(--ember-light-9);
|
|
||||||
--text-base: var(--ink-light-11);
|
|
||||||
--input-base: var(--ink-light-1);
|
|
||||||
--input-hover: var(--ink-light-2);
|
|
||||||
--input-active: var(--cobalt-light-1);
|
|
||||||
--input-selected: var(--cobalt-light-4);
|
|
||||||
--input-focus: var(--cobalt-light-1);
|
|
||||||
--input-disabled: var(--ink-light-4);
|
|
||||||
--text-weak: var(--ink-light-9);
|
|
||||||
--text-weaker: var(--ink-light-8);
|
|
||||||
--text-strong: var(--ink-light-12);
|
|
||||||
--text-interactive-base: var(--cobalt-light-9);
|
|
||||||
--text-on-brand-base: var(--ink-light-alpha-11);
|
|
||||||
--text-on-interactive-base: var(--ink-light-1);
|
|
||||||
--text-on-interactive-weak: var(--ink-light-alpha-11);
|
|
||||||
--text-on-success-base: var(--apple-light-10);
|
|
||||||
--text-on-critical-base: var(--ember-light-10);
|
|
||||||
--text-on-critical-weak: var(--ember-light-8);
|
|
||||||
--text-on-critical-strong: var(--ember-light-12);
|
|
||||||
--text-on-warning-base: var(--ink-dark-alpha-11);
|
|
||||||
--text-on-info-base: var(--ink-dark-alpha-11);
|
|
||||||
--text-diff-add-base: var(--mint-light-11);
|
|
||||||
--text-diff-delete-base: var(--ember-light-10);
|
|
||||||
--text-diff-delete-strong: var(--ember-light-12);
|
|
||||||
--text-diff-add-strong: var(--mint-light-12);
|
|
||||||
--text-on-info-weak: var(--ink-dark-alpha-9);
|
|
||||||
--text-on-info-strong: var(--ink-dark-alpha-12);
|
|
||||||
--text-on-warning-weak: var(--ink-dark-alpha-9);
|
|
||||||
--text-on-warning-strong: var(--ink-dark-alpha-12);
|
|
||||||
--text-on-success-weak: var(--apple-light-6);
|
|
||||||
--text-on-success-strong: var(--apple-light-12);
|
|
||||||
--text-on-brand-weak: var(--ink-light-alpha-9);
|
|
||||||
--text-on-brand-weaker: var(--ink-light-alpha-8);
|
|
||||||
--text-on-brand-strong: var(--ink-light-alpha-12);
|
|
||||||
--button-secondary-base: #fcfdfd;
|
|
||||||
--button-secondary-hover: #f9fafa;
|
|
||||||
--border-base: var(--ink-light-alpha-7);
|
|
||||||
--border-hover: var(--ink-light-alpha-8);
|
|
||||||
--border-active: var(--ink-light-alpha-9);
|
|
||||||
--border-selected: var(--cobalt-light-alpha-9);
|
|
||||||
--border-disabled: var(--ink-light-alpha-8);
|
|
||||||
--border-focus: var(--ink-light-alpha-9);
|
|
||||||
--border-weak-base: var(--ink-light-alpha-5);
|
|
||||||
--border-strong-base: var(--ink-light-alpha-7);
|
|
||||||
--border-strong-hover: var(--ink-light-alpha-8);
|
|
||||||
--border-strong-active: var(--ink-light-alpha-7);
|
|
||||||
--border-strong-selected: var(--cobalt-light-alpha-6);
|
|
||||||
--border-strong-disabled: var(--ink-light-alpha-6);
|
|
||||||
--border-strong-focus: var(--ink-light-alpha-7);
|
|
||||||
--border-weak-hover: var(--ink-light-alpha-6);
|
|
||||||
--border-weak-active: var(--ink-light-alpha-7);
|
|
||||||
--border-weak-selected: var(--cobalt-light-alpha-5);
|
|
||||||
--border-weak-disabled: var(--ink-light-alpha-6);
|
|
||||||
--border-weak-focus: var(--ink-light-alpha-7);
|
|
||||||
--border-interactive-base: var(--cobalt-light-7);
|
|
||||||
--border-interactive-hover: var(--cobalt-light-8);
|
|
||||||
--border-interactive-active: var(--cobalt-light-9);
|
|
||||||
--border-interactive-selected: var(--cobalt-light-9);
|
|
||||||
--border-interactive-disabled: var(--ink-light-8);
|
|
||||||
--border-interactive-focus: var(--cobalt-light-9);
|
|
||||||
--border-success-base: var(--apple-light-6);
|
|
||||||
--border-success-hover: var(--apple-light-7);
|
|
||||||
--border-success-selected: var(--apple-light-9);
|
|
||||||
--border-warning-base: var(--solaris-light-6);
|
|
||||||
--border-warning-hover: var(--solaris-light-7);
|
|
||||||
--border-warning-selected: var(--solaris-light-9);
|
|
||||||
--border-critical-base: var(--ember-light-6);
|
|
||||||
--border-critical-hover: var(--ember-light-7);
|
|
||||||
--border-critical-selected: var(--ember-light-9);
|
|
||||||
--border-info-base: var(--lilac-light-6);
|
|
||||||
--border-info-hover: var(--lilac-light-7);
|
|
||||||
--border-info-selected: var(--lilac-light-9);
|
|
||||||
--icon-base: var(--ink-light-9);
|
|
||||||
--icon-hover: var(--ink-light-11);
|
|
||||||
--icon-active: var(--ink-light-12);
|
|
||||||
--icon-selected: var(--ink-light-12);
|
|
||||||
--icon-disabled: var(--ink-light-8);
|
|
||||||
--icon-focus: var(--ink-light-12);
|
|
||||||
--icon-invert-base: #ffffff;
|
|
||||||
--icon-weak-base: var(--ink-light-7);
|
|
||||||
--icon-weak-hover: var(--ink-light-8);
|
|
||||||
--icon-weak-active: var(--ink-light-9);
|
|
||||||
--icon-weak-selected: var(--ink-light-10);
|
|
||||||
--icon-weak-disabled: var(--ink-light-6);
|
|
||||||
--icon-weak-focus: var(--ink-light-9);
|
|
||||||
--icon-strong-base: var(--ink-light-12);
|
|
||||||
--icon-strong-hover: #131515;
|
|
||||||
--icon-strong-active: #020202;
|
|
||||||
--icon-strong-selected: #020202;
|
|
||||||
--icon-strong-disabled: var(--ink-light-8);
|
|
||||||
--icon-strong-focus: #020202;
|
|
||||||
--icon-brand-base: var(--ink-light-12);
|
|
||||||
--icon-interactive-base: var(--cobalt-light-9);
|
|
||||||
--icon-success-base: var(--apple-light-7);
|
|
||||||
--icon-success-hover: var(--apple-light-8);
|
|
||||||
--icon-success-active: var(--apple-light-11);
|
|
||||||
--icon-warning-base: var(--amber-light-7);
|
|
||||||
--icon-warning-hover: var(--amber-light-8);
|
|
||||||
--icon-warning-active: var(--amber-light-11);
|
|
||||||
--icon-critical-base: var(--ember-light-10);
|
|
||||||
--icon-critical-hover: var(--ember-light-11);
|
|
||||||
--icon-critical-active: var(--ember-light-12);
|
|
||||||
--icon-info-base: var(--lilac-light-7);
|
|
||||||
--icon-info-hover: var(--lilac-light-8);
|
|
||||||
--icon-info-active: var(--lilac-light-11);
|
|
||||||
--icon-on-brand-base: var(--ink-light-alpha-11);
|
|
||||||
--icon-on-brand-hover: var(--ink-light-alpha-12);
|
|
||||||
--icon-on-brand-selected: var(--ink-light-alpha-12);
|
|
||||||
--icon-on-interactive-base: var(--ink-light-1);
|
|
||||||
--icon-agent-plan-base: var(--purple-light-9);
|
|
||||||
--icon-agent-docs-base: var(--amber-light-9);
|
|
||||||
--icon-agent-ask-base: var(--cyan-light-9);
|
|
||||||
--icon-agent-build-base: var(--cobalt-light-9);
|
|
||||||
--icon-on-success-base: var(--apple-light-alpha-9);
|
|
||||||
--icon-on-success-hover: var(--apple-light-alpha-10);
|
|
||||||
--icon-on-success-selected: var(--apple-light-alpha-11);
|
|
||||||
--icon-on-warning-base: var(--amber-lightalpha-9);
|
|
||||||
--icon-on-warning-hover: var(--amber-lightalpha-10);
|
|
||||||
--icon-on-warning-selected: var(--amber-lightalpha-11);
|
|
||||||
--icon-on-critical-base: var(--ember-light-alpha-9);
|
|
||||||
--icon-on-critical-hover: var(--ember-light-alpha-10);
|
|
||||||
--icon-on-critical-selected: var(--ember-light-alpha-11);
|
|
||||||
--icon-on-info-base: var(--lilac-light-9);
|
|
||||||
--icon-on-info-hover: var(--lilac-light-alpha-10);
|
|
||||||
--icon-on-info-selected: var(--lilac-light-alpha-11);
|
|
||||||
--icon-diff-add-base: var(--mint-light-11);
|
|
||||||
--icon-diff-add-hover: var(--mint-light-12);
|
|
||||||
--icon-diff-add-active: var(--mint-light-12);
|
|
||||||
--icon-diff-delete-base: var(--ember-light-10);
|
|
||||||
--icon-diff-delete-hover: var(--ember-light-11);
|
|
||||||
--syntax-comment: var(--text-weak);
|
|
||||||
--syntax-regexp: var(--text-base);
|
|
||||||
--syntax-string: #007663;
|
|
||||||
--syntax-keyword: var(--text-weak);
|
|
||||||
--syntax-primitive: #fb7f51;
|
|
||||||
--syntax-operator: var(--text-weak);
|
|
||||||
--syntax-variable: var(--text-strong);
|
|
||||||
--syntax-property: #ec6cc8;
|
|
||||||
--syntax-type: #738400;
|
|
||||||
--syntax-constant: #00b2b9;
|
|
||||||
--syntax-punctuation: var(--text-weak);
|
|
||||||
--syntax-object: var(--text-strong);
|
|
||||||
--syntax-success: var(--apple-light-10);
|
|
||||||
--syntax-warning: var(--amber-light-10);
|
|
||||||
--syntax-critical: var(--ember-light-9);
|
|
||||||
--syntax-info: #0091a7;
|
|
||||||
--syntax-diff-add: var(--mint-light-11);
|
|
||||||
--syntax-diff-delete: var(--ember-light-11);
|
|
||||||
--syntax-diff-unknown: #ff0000;
|
|
||||||
--markdown-heading: #d68c27;
|
|
||||||
--markdown-text: #1a1a1a;
|
|
||||||
--markdown-link: #3b7dd8;
|
|
||||||
--markdown-link-text: #318795;
|
|
||||||
--markdown-code: #3d9a57;
|
|
||||||
--markdown-block-quote: #b0851f;
|
|
||||||
--markdown-emph: #b0851f;
|
|
||||||
--markdown-strong: #d68c27;
|
|
||||||
--markdown-horizontal-rule: #8a8a8a;
|
|
||||||
--markdown-list-item: #3b7dd8;
|
|
||||||
--markdown-list-enumeration: #318795;
|
|
||||||
--markdown-image: #3b7dd8;
|
|
||||||
--markdown-image-text: #318795;
|
|
||||||
--markdown-code-block: #1a1a1a;
|
|
||||||
--border-color: #ffffff;
|
|
||||||
--border-weaker-base: var(--ink-light-alpha-3);
|
|
||||||
--border-weaker-hover: var(--ink-light-alpha-4);
|
|
||||||
--border-weaker-active: var(--ink-light-alpha-6);
|
|
||||||
--border-weaker-selected: var(--cobalt-light-alpha-4);
|
|
||||||
--border-weaker-disabled: var(--ink-light-alpha-2);
|
|
||||||
--border-weaker-focus: var(--ink-light-alpha-6);
|
|
||||||
--button-ghost-hover: var(--ink-light-alpha-2);
|
|
||||||
--button-ghost-hover2: var(--ink-light-alpha-3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user