mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-12 03:44:28 +00:00
Compare commits
314 Commits
thinking-t
...
github-v1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e3ad770ac | ||
|
|
87524de265 | ||
|
|
ee10d9b898 | ||
|
|
bbd36e8441 | ||
|
|
4e2d1acf7d | ||
|
|
40d63cd1e3 | ||
|
|
77b2331428 | ||
|
|
2b7e2edee5 | ||
|
|
28aba35ff9 | ||
|
|
89219a77f7 | ||
|
|
20e3a74bad | ||
|
|
ff690350b1 | ||
|
|
ebefb26e8f | ||
|
|
0b1ee9ddd9 | ||
|
|
79599f351e | ||
|
|
8c9f6b1d3e | ||
|
|
83bcb9e95b | ||
|
|
96b9ff8d0e | ||
|
|
0af2254856 | ||
|
|
c2944024a8 | ||
|
|
5be4bda90f | ||
|
|
b78e2db013 | ||
|
|
3f4d1121a4 | ||
|
|
def910021d | ||
|
|
3ac42e9632 | ||
|
|
9c26bb7c6c | ||
|
|
53f20f7612 | ||
|
|
11b3927dc2 | ||
|
|
a190eda2c8 | ||
|
|
1f18f389c0 | ||
|
|
84e56ee614 | ||
|
|
59329a414d | ||
|
|
452c991f58 | ||
|
|
be8116e2ea | ||
|
|
f0ed1e38c9 | ||
|
|
ac0f1dbbdd | ||
|
|
275a352e81 | ||
|
|
9f3bc0e352 | ||
|
|
6c1a1a77b7 | ||
|
|
2e21c62320 | ||
|
|
19c6fec4d1 | ||
|
|
4779d99a13 | ||
|
|
05e0759878 | ||
|
|
2330ec6dc3 | ||
|
|
75e5130cf8 | ||
|
|
87efd27459 | ||
|
|
62f080b0e4 | ||
|
|
ae3990a557 | ||
|
|
d7b5b431d6 | ||
|
|
e2fbd098d2 | ||
|
|
ef78fd8bae | ||
|
|
72ebaeb8f7 | ||
|
|
0dc62d5dad | ||
|
|
d118782a10 | ||
|
|
ff05647350 | ||
|
|
0e1c711c4e | ||
|
|
bfb254dac6 | ||
|
|
92fe927785 | ||
|
|
2e25fe9d5d | ||
|
|
38c5f23f4a | ||
|
|
112c58abf5 | ||
|
|
0dce5173cc | ||
|
|
2c70c0b00f | ||
|
|
34024c2504 | ||
|
|
27e826eba6 | ||
|
|
89a4f1c1ae | ||
|
|
c0c61b25ff | ||
|
|
0d1c6e0ca9 | ||
|
|
002db3abf4 | ||
|
|
416a919c6d | ||
|
|
dbbcf0b8d0 | ||
|
|
efac8cebb3 | ||
|
|
4f2baf1a72 | ||
|
|
48b2bde6e5 | ||
|
|
88314148e6 | ||
|
|
56452d886d | ||
|
|
f3e64cfb19 | ||
|
|
8fcc80bc20 | ||
|
|
0beccc406e | ||
|
|
b82ea693db | ||
|
|
4fd9a19fbb | ||
|
|
e16487b804 | ||
|
|
5388192aac | ||
|
|
8010448ba1 | ||
|
|
66f3e69867 | ||
|
|
ca599ab8fc | ||
|
|
c3b3b133b0 | ||
|
|
300ec0e0af | ||
|
|
6632987827 | ||
|
|
e555e893c4 | ||
|
|
81134cf61e | ||
|
|
37e4c1e619 | ||
|
|
02b5e7d72c | ||
|
|
7abc2a947e | ||
|
|
337a7e9646 | ||
|
|
62cc532ecc | ||
|
|
d5a506d4ae | ||
|
|
9c5f94bd66 | ||
|
|
83390314d6 | ||
|
|
8b08e9cda5 | ||
|
|
b1b1df824c | ||
|
|
7d1733c752 | ||
|
|
cf05e6e02b | ||
|
|
7e49d0fb15 | ||
|
|
c4f63824df | ||
|
|
4236744fb5 | ||
|
|
284c045795 | ||
|
|
2c53abd70c | ||
|
|
b7a9cbfc68 | ||
|
|
46a35dfc1b | ||
|
|
b7597c12dd | ||
|
|
6830590183 | ||
|
|
b9b4349039 | ||
|
|
4107918909 | ||
|
|
6347ee9988 | ||
|
|
9daa4e04ea | ||
|
|
ed96ae9d45 | ||
|
|
8ce0966987 | ||
|
|
8cb26b6066 | ||
|
|
5cf6a1343c | ||
|
|
44d6c5780d | ||
|
|
5eaa8e1bf4 | ||
|
|
df2713a6c2 | ||
|
|
ff6864a7ca | ||
|
|
5e37a902ce | ||
|
|
df2ebfac7d | ||
|
|
5fbcb203f5 | ||
|
|
34db739442 | ||
|
|
ae8c4154aa | ||
|
|
315836c0b7 | ||
|
|
c0d009d5f3 | ||
|
|
c36f3b9dbe | ||
|
|
d31824320e | ||
|
|
88c0675148 | ||
|
|
82c4755fb0 | ||
|
|
40572eeba4 | ||
|
|
d81d63045a | ||
|
|
ece3bfd93d | ||
|
|
acd91bddf7 | ||
|
|
3a14ca044c | ||
|
|
d66d806700 | ||
|
|
e9b95b2e91 | ||
|
|
56dde2cc83 | ||
|
|
d2ce368a3f | ||
|
|
f492122d59 | ||
|
|
b0f77da56c | ||
|
|
274b86b19b | ||
|
|
2ca118db59 | ||
|
|
a0c0e2b5c3 | ||
|
|
d43fbec12d | ||
|
|
bb426112ed | ||
|
|
d2217bb825 | ||
|
|
ac495bd351 | ||
|
|
b913eb7acc | ||
|
|
ea65a91b2e | ||
|
|
ed6d749104 | ||
|
|
9eefcd1b41 | ||
|
|
7c1124199e | ||
|
|
5cf126d489 | ||
|
|
509f7d9617 | ||
|
|
ae1bf92c81 | ||
|
|
b021b26e77 | ||
|
|
9555d348de | ||
|
|
220c564047 | ||
|
|
cf5c0129ac | ||
|
|
543dbe71d2 | ||
|
|
54569b5552 | ||
|
|
6a09861806 | ||
|
|
79a4c65313 | ||
|
|
654534ac71 | ||
|
|
ba16bfdf3d | ||
|
|
ad5614bbb9 | ||
|
|
dda579c8ad | ||
|
|
4246cdb069 | ||
|
|
7ade6d386d | ||
|
|
2613f44961 | ||
|
|
62ffeb3987 | ||
|
|
4a8e8f537c | ||
|
|
a68bee7878 | ||
|
|
ed33d82535 | ||
|
|
2d63c22d1a | ||
|
|
e22af25076 | ||
|
|
622caae9c9 | ||
|
|
fed4776451 | ||
|
|
fdf560c343 | ||
|
|
fc3ffb2bf9 | ||
|
|
7368342bab | ||
|
|
c8fc910533 | ||
|
|
0f9ef84d55 | ||
|
|
74b5c285cf | ||
|
|
a34e67b518 | ||
|
|
0c7f0cfa2e | ||
|
|
10ee6d345b | ||
|
|
48ec68730f | ||
|
|
70e4efe429 | ||
|
|
92948ed8a4 | ||
|
|
6d412d8872 | ||
|
|
e6a0a005d6 | ||
|
|
90d44751e7 | ||
|
|
4d062ba1b2 | ||
|
|
f8bca50f00 | ||
|
|
3d2ef28fa8 | ||
|
|
210b3e905b | ||
|
|
96975ef8d6 | ||
|
|
b8b998be56 | ||
|
|
d8ac35f6e5 | ||
|
|
ed1eacfce0 | ||
|
|
629f475f63 | ||
|
|
43a7c1dd8c | ||
|
|
e288ce0fca | ||
|
|
67b3fcb31a | ||
|
|
aedb5550a8 | ||
|
|
1638ffde69 | ||
|
|
d4cfbd8219 | ||
|
|
c7bac83212 | ||
|
|
fc9789d7a7 | ||
|
|
a8957d8d16 | ||
|
|
0660433921 | ||
|
|
1a6f4f1c0d | ||
|
|
974a24ba02 | ||
|
|
5ebe29de1e | ||
|
|
6bdf8b1fe1 | ||
|
|
5bcc93851c | ||
|
|
d0789632b4 | ||
|
|
a6e297baad | ||
|
|
307af10c8b | ||
|
|
f254cf76d9 | ||
|
|
b4ffaa21ec | ||
|
|
7bf6f264e4 | ||
|
|
7434fbba8e | ||
|
|
b7581e01ea | ||
|
|
b46d4789fc | ||
|
|
199bd8a9a2 | ||
|
|
decf2452c4 | ||
|
|
d8663a44c2 | ||
|
|
8917a4c609 | ||
|
|
5d7a52f8b8 | ||
|
|
b7b827c5bd | ||
|
|
613e082358 | ||
|
|
b6856bd593 | ||
|
|
7cb5a77ba6 | ||
|
|
cd9898a565 | ||
|
|
a4ffa869cc | ||
|
|
dbc84ff4c3 | ||
|
|
c11ea3fd92 | ||
|
|
3c3a0f8afb | ||
|
|
b93614cb81 | ||
|
|
b84d513bd7 | ||
|
|
0554d03162 | ||
|
|
15caecdb45 | ||
|
|
91ab966921 | ||
|
|
bc3286de46 | ||
|
|
af45444496 | ||
|
|
43202f2820 | ||
|
|
ce37e11bfe | ||
|
|
6e9833acce | ||
|
|
379c4ecab3 | ||
|
|
f1db4b60c4 | ||
|
|
9846b26be7 | ||
|
|
d6ba6af6f3 | ||
|
|
d463ade028 | ||
|
|
6c3495a75a | ||
|
|
a16edb4ea0 | ||
|
|
9efe09564b | ||
|
|
ccdd77032a | ||
|
|
41e234c6d0 | ||
|
|
3e03646e42 | ||
|
|
f7acc34327 | ||
|
|
bf420e7df6 | ||
|
|
78484f545c | ||
|
|
ad008d2151 | ||
|
|
651a10d6db | ||
|
|
f9674793af | ||
|
|
f3a33d41f1 | ||
|
|
642eec3dfd | ||
|
|
73513612d4 | ||
|
|
9b77246246 | ||
|
|
cf3bc1e0a6 | ||
|
|
4550ad049e | ||
|
|
d51c6ca39f | ||
|
|
47c6a2430c | ||
|
|
909013320b | ||
|
|
770a4d87db | ||
|
|
2e417c4d8c | ||
|
|
2da527aaa6 | ||
|
|
0303eb0cc1 | ||
|
|
3f4a792c8a | ||
|
|
9629f7464b | ||
|
|
9017d10303 | ||
|
|
59d4041aa4 | ||
|
|
feb8c4f3c6 | ||
|
|
3f5cd2c4a8 | ||
|
|
a160eb76df | ||
|
|
e4b2028f91 | ||
|
|
ffc48e115b | ||
|
|
04b4dacee3 | ||
|
|
c0e30f48c6 | ||
|
|
99158e736b | ||
|
|
4c02d515a1 | ||
|
|
b803a9732d | ||
|
|
f9d5e18790 | ||
|
|
147111c9c6 | ||
|
|
9a70eb538b | ||
|
|
0b1731142e | ||
|
|
7ec48dfd15 | ||
|
|
57120e69ed | ||
|
|
11efda3f5c | ||
|
|
a5cb4e41f5 | ||
|
|
88b2382b97 | ||
|
|
237c0253c2 | ||
|
|
a9f27371cf | ||
|
|
9c126c5b64 | ||
|
|
e241aa21b9 | ||
|
|
1fd005838c |
63
.github/workflows/auto-label-tui.yml
vendored
63
.github/workflows/auto-label-tui.yml
vendored
@@ -1,63 +0,0 @@
|
||||
name: Auto-label TUI Issues
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
auto-label:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- name: Auto-label and assign issues
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
const title = issue.title;
|
||||
const description = issue.body || '';
|
||||
|
||||
// Check for "opencode web" keyword
|
||||
const webPattern = /(opencode web)/i;
|
||||
const isWebRelated = webPattern.test(title) || webPattern.test(description);
|
||||
|
||||
// Check for version patterns like v1.0.x or 1.0.x
|
||||
const versionPattern = /[v]?1\.0\./i;
|
||||
const isVersionRelated = versionPattern.test(title) || versionPattern.test(description);
|
||||
|
||||
// Check for "nix" keyword
|
||||
const nixPattern = /\bnix\b/i;
|
||||
const isNixRelated = nixPattern.test(title) || nixPattern.test(description);
|
||||
|
||||
const labels = [];
|
||||
|
||||
if (isWebRelated) {
|
||||
labels.push('web');
|
||||
|
||||
// Assign to adamdotdevin
|
||||
await github.rest.issues.addAssignees({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
assignees: ['adamdotdevin']
|
||||
});
|
||||
} else if (isVersionRelated) {
|
||||
// Only add opentui if NOT web-related
|
||||
labels.push('opentui');
|
||||
}
|
||||
|
||||
if (isNixRelated) {
|
||||
labels.push('nix');
|
||||
}
|
||||
|
||||
if (labels.length > 0) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: labels
|
||||
});
|
||||
}
|
||||
2
.github/workflows/opencode.yml
vendored
2
.github/workflows/opencode.yml
vendored
@@ -31,4 +31,4 @@ jobs:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
OPENCODE_PERMISSION: '{"bash": "deny"}'
|
||||
with:
|
||||
model: opencode/claude-haiku-4-5
|
||||
model: opencode/claude-opus-4-5
|
||||
|
||||
35
.github/workflows/publish.yml
vendored
35
.github/workflows/publish.yml
vendored
@@ -21,7 +21,7 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.version || inputs.bump }}
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
@@ -31,7 +31,7 @@ permissions:
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
if: github.repository == 'sst/opencode' && github.ref == 'refs/heads/dev'
|
||||
if: github.repository == 'sst/opencode'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
@@ -64,6 +64,12 @@ jobs:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
@@ -103,6 +109,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ needs.publish.outputs.tagName }}
|
||||
|
||||
- uses: apple-actions/import-codesign-certs@v2
|
||||
if: ${{ runner.os == 'macOS' }}
|
||||
@@ -158,12 +165,18 @@ jobs:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
RUST_TARGET: ${{ matrix.settings.target }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
OPENCODE_RELEASE_TAG: ${{ needs.publish.outputs.tagName }}
|
||||
|
||||
# Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
|
||||
- run: cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage
|
||||
- name: Install tauri-cli from portable appimage branch
|
||||
if: contains(matrix.settings.host, 'ubuntu')
|
||||
run: |
|
||||
cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage --force
|
||||
echo "Installed tauri-cli version:"
|
||||
cargo tauri --version
|
||||
|
||||
- name: Build and upload artifacts
|
||||
timeout-minutes: 20
|
||||
uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -185,3 +198,19 @@ jobs:
|
||||
releaseId: ${{ needs.publish.outputs.releaseId }}
|
||||
tagName: ${{ needs.publish.outputs.tagName }}
|
||||
releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
|
||||
releaseDraft: true
|
||||
|
||||
publish-release:
|
||||
needs:
|
||||
- publish
|
||||
- publish-tauri
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ needs.publish.outputs.tagName }}
|
||||
|
||||
- run: gh release edit ${{ steps.publish.outputs.tagName }} --draft=false
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
29
.github/workflows/release-github-action.yml
vendored
Normal file
29
.github/workflows/release-github-action.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: release-github-action
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "github/**"
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- run: git fetch --force --tags
|
||||
|
||||
- name: Release
|
||||
run: |
|
||||
git config --global user.email "opencode@sst.dev"
|
||||
git config --global user.name "opencode"
|
||||
./github/script/release
|
||||
37
.github/workflows/triage.yml
vendored
Normal file
37
.github/workflows/triage.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Issue Triage
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: Install opencode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
- name: Triage issue
|
||||
env:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||
ISSUE_BODY: ${{ github.event.issue.body }}
|
||||
run: |
|
||||
opencode run --agent triage "The following issue was just opened, triage it:
|
||||
|
||||
Title: $ISSUE_TITLE
|
||||
|
||||
$ISSUE_BODY"
|
||||
69
.opencode/agent/triage.md
Normal file
69
.opencode/agent/triage.md
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
mode: primary
|
||||
hidden: true
|
||||
model: opencode/claude-haiku-4-5
|
||||
tools:
|
||||
"*": false
|
||||
"github-triage": true
|
||||
---
|
||||
|
||||
You are a triage agent responsible for triaging github issues.
|
||||
|
||||
Use your github-triage tool to triage issues.
|
||||
|
||||
## Labels
|
||||
|
||||
#### perf
|
||||
|
||||
Performance-related issues:
|
||||
|
||||
- Slow performance
|
||||
- High RAM usage
|
||||
- High CPU usage
|
||||
|
||||
**Only** add if it's likely a RAM or CPU issue. **Do not** add for LLM slowness.
|
||||
|
||||
#### desktop
|
||||
|
||||
Desktop app issues:
|
||||
|
||||
- `opencode web` command
|
||||
- The desktop app itself
|
||||
|
||||
**Only** add if it's specifically about the Desktop application or `opencode web` view. **Do not** add for terminal, TUI, or general opencode issues.
|
||||
|
||||
#### nix
|
||||
|
||||
**Only** add if the issue explicitly mentions nix.
|
||||
|
||||
#### zen
|
||||
|
||||
**Only** add if the issue mentions "zen" or "opencode zen". Zen is our gateway for coding models. **Do not** add for other gateways or inference providers.
|
||||
|
||||
#### docs
|
||||
|
||||
Add if the issue requests better documentation or docs updates.
|
||||
|
||||
#### opentui
|
||||
|
||||
TUI issues potentially caused by our underlying TUI library:
|
||||
|
||||
- Keybindings not working
|
||||
- Scroll speed issues (too fast/slow/laggy)
|
||||
- Screen flickering
|
||||
- Crashes with opentui in the log
|
||||
|
||||
**Do not** add for general TUI bugs.
|
||||
|
||||
When assigning to people here are the following rules:
|
||||
|
||||
adamdotdev:
|
||||
ONLY assign adam if the issue will have the "desktop" label.
|
||||
|
||||
fwang:
|
||||
ONLY assign fwang if the issue will have the "zen" label.
|
||||
|
||||
jayair:
|
||||
ONLY assign jayair if the issue will have the "docs" label.
|
||||
|
||||
In all other cases use best judgment. Avoid assigning to kommander needlessly, when in doubt assign to rekram1-node.
|
||||
4
.opencode/env.d.ts
vendored
Normal file
4
.opencode/env.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module "*.txt" {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
@@ -10,4 +10,8 @@
|
||||
"options": {},
|
||||
},
|
||||
},
|
||||
"mcp": {},
|
||||
"tools": {
|
||||
"github-triage": false,
|
||||
},
|
||||
}
|
||||
|
||||
66
.opencode/tool/github-triage.ts
Normal file
66
.opencode/tool/github-triage.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/// <reference path="../env.d.ts" />
|
||||
import { Octokit } from "@octokit/rest"
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
import DESCRIPTION from "./github-triage.txt"
|
||||
|
||||
function getIssueNumber(): number {
|
||||
const issue = parseInt(process.env.ISSUE_NUMBER ?? "", 10)
|
||||
if (!issue) throw new Error("ISSUE_NUMBER env var not set")
|
||||
return issue
|
||||
}
|
||||
|
||||
export default tool({
|
||||
description: DESCRIPTION,
|
||||
args: {
|
||||
assignee: tool.schema
|
||||
.enum(["thdxr", "adamdotdevin", "rekram1-node", "fwang", "jayair", "kommander"])
|
||||
.describe("The username of the assignee")
|
||||
.default("rekram1-node"),
|
||||
labels: tool.schema
|
||||
.array(tool.schema.enum(["nix", "opentui", "perf", "desktop", "zen", "docs"]))
|
||||
.describe("The labels(s) to add to the issue")
|
||||
.default([]),
|
||||
},
|
||||
async execute(args) {
|
||||
const issue = getIssueNumber()
|
||||
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN })
|
||||
const owner = "sst"
|
||||
const repo = "opencode"
|
||||
|
||||
const results: string[] = []
|
||||
|
||||
if (args.assignee === "adamdotdevin" && !args.labels.includes("desktop")) {
|
||||
throw new Error("Only desktop issues should be assigned to adamdotdevin")
|
||||
}
|
||||
|
||||
if (args.assignee === "fwang" && !args.labels.includes("zen")) {
|
||||
throw new Error("Only zen issues should be assigned to fwang")
|
||||
}
|
||||
|
||||
if (args.assignee === "kommander" && !args.labels.includes("opentui")) {
|
||||
throw new Error("Only opentui issues should be assigned to kommander")
|
||||
}
|
||||
|
||||
await octokit.rest.issues.addAssignees({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue,
|
||||
assignees: [args.assignee],
|
||||
})
|
||||
results.push(`Assigned @${args.assignee} to issue #${issue}`)
|
||||
|
||||
const labels: string[] = args.labels.map((label) => (label === "desktop" ? "web" : label))
|
||||
|
||||
if (labels.length > 0) {
|
||||
await octokit.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue,
|
||||
labels,
|
||||
})
|
||||
results.push(`Added labels: ${args.labels.join(", ")}`)
|
||||
}
|
||||
|
||||
return results.join("\n")
|
||||
},
|
||||
})
|
||||
84
.opencode/tool/github-triage.txt
Normal file
84
.opencode/tool/github-triage.txt
Normal file
@@ -0,0 +1,84 @@
|
||||
Use this tool to assign and/or label a Github issue.
|
||||
|
||||
You can assign the following users:
|
||||
- thdxr
|
||||
- adamdotdevin
|
||||
- fwang
|
||||
- jayair
|
||||
- kommander
|
||||
- rekram1-node
|
||||
|
||||
|
||||
You can use the following labels:
|
||||
- nix
|
||||
- opentui
|
||||
- perf
|
||||
- web
|
||||
- zen
|
||||
- docs
|
||||
|
||||
Always try to assign an issue, if in doubt, assign rekram1-node to it.
|
||||
|
||||
## Breakdown of responsibilities:
|
||||
|
||||
### thdxr
|
||||
|
||||
Dax is responsible for managing core parts of the application, for large feature requests, api changes, or things that require significant changes to the codebase assign him.
|
||||
|
||||
This relates to OpenCode server primarily but has overlap with just about anything
|
||||
|
||||
### adamdotdevin
|
||||
|
||||
Adam is responsible for managing the Desktop/Web app. If there is an issue relating to the desktop app or `opencode web` command. Assign him.
|
||||
|
||||
|
||||
### fwang
|
||||
|
||||
Frank is responsible for managing Zen, if you see complaints about OpenCode Zen, maybe it's the dashboard, the model quality, billing issues, etc. Assign him to the issue.
|
||||
|
||||
### jayair
|
||||
|
||||
Jay is responsible for documentation. If there is an issue relating to documentation assign him.
|
||||
|
||||
### kommander
|
||||
|
||||
Sebastian is responsible for managing an OpenTUI (a library for building terminal user interfaces). OpenCode's TUI is built with OpenTUI. If there are issues about:
|
||||
- random characters on screen
|
||||
- keybinds not working on different terminals
|
||||
- general terminal stuff
|
||||
Then assign the issue to Him.
|
||||
|
||||
### rekram1-node
|
||||
|
||||
ALL BUGS SHOULD BE assigned to rekram1-node unless they have the `opentui` label.
|
||||
|
||||
Assign Aiden to an issue as a catch all, if you can't assign anyone else. Most of the time this will be bugs/polish things.
|
||||
If no one else makes sense to assign, assign rekram1-node to it.
|
||||
|
||||
Always assign to aiden if the issue mentions "acp", "zed", or model performance issues
|
||||
|
||||
## Breakdown of Labels:
|
||||
|
||||
### nix
|
||||
|
||||
Any issue that mentions nix, or nixos should have a nix label
|
||||
|
||||
### opentui
|
||||
|
||||
Anything relating to the TUI itself should have an opentui label
|
||||
|
||||
### perf
|
||||
|
||||
Anything related to slow performance, high ram, high cpu usage, or any other performance related issue should have a perf label
|
||||
|
||||
### desktop
|
||||
|
||||
Anything related to `opencode web` command or the desktop app should have a desktop label. Never add this label for anything terminal/tui related
|
||||
|
||||
### zen
|
||||
|
||||
Anything related to OpenCode Zen, billing, or model quality from Zen should have a zen label
|
||||
|
||||
### docs
|
||||
|
||||
Anything related to the documentation should have a docs label
|
||||
16
README.md
16
README.md
@@ -37,6 +37,22 @@ nix run nixpkgs#opencode # or github:sst/opencode for latest dev branc
|
||||
> [!TIP]
|
||||
> Remove versions older than 0.1.x before installing.
|
||||
|
||||
### Desktop App (BETA)
|
||||
|
||||
OpenCode is also available as a desktop application. Download directly from the [releases page](https://github.com/sst/opencode/releases) or [opencode.ai/download](https://opencode.ai/download).
|
||||
|
||||
| Platform | Download |
|
||||
| --------------------- | ------------------------------------- |
|
||||
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
|
||||
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
|
||||
| Windows | `opencode-desktop-windows-x64.exe` |
|
||||
| Linux | `.deb`, `.rpm`, or AppImage |
|
||||
|
||||
```bash
|
||||
# macOS (Homebrew)
|
||||
brew install --cask opencode-desktop
|
||||
```
|
||||
|
||||
#### Installation Directory
|
||||
|
||||
The install script respects the following priority order for the installation path:
|
||||
|
||||
5
STATS.md
5
STATS.md
@@ -167,3 +167,8 @@
|
||||
| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773) | 1,985,410 (+27,363) |
|
||||
| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
|
||||
| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
|
||||
| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
|
||||
| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) |
|
||||
| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) |
|
||||
| 2025-12-15 | 1,093,632 (+11,590) | 1,059,078 (+6,653) | 2,152,710 (+18,243) |
|
||||
| 2025-12-16 | 1,120,477 (+26,845) | 1,078,022 (+18,944) | 2,198,499 (+45,789) |
|
||||
|
||||
134
bun.lock
134
bun.lock
@@ -6,6 +6,7 @@
|
||||
"name": "opencode",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.933.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"typescript": "catalog:",
|
||||
@@ -20,7 +21,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.150",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -48,7 +49,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.150",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -75,7 +76,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.150",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
@@ -99,7 +100,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.150",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -123,7 +124,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.150",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -131,7 +132,9 @@
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@shikijs/transformers": "3.9.2",
|
||||
"@solid-primitives/active-element": "2.1.3",
|
||||
"@solid-primitives/audio": "1.4.2",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
"@solid-primitives/media": "2.3.3",
|
||||
"@solid-primitives/resize-observer": "2.1.3",
|
||||
"@solid-primitives/scroll": "2.1.3",
|
||||
"@solid-primitives/storage": "4.3.3",
|
||||
@@ -168,11 +171,11 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.150",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@pierre/precision-diffs": "catalog:",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@solidjs/router": "catalog:",
|
||||
"@solidjs/start": "catalog:",
|
||||
@@ -197,10 +200,10 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.150",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@octokit/rest": "catalog:",
|
||||
"hono": "catalog:",
|
||||
"jose": "6.0.11",
|
||||
},
|
||||
@@ -213,7 +216,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.0.150",
|
||||
"version": "1.0.163",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -236,7 +239,7 @@
|
||||
"@hono/zod-validator": "catalog:",
|
||||
"@modelcontextprotocol/sdk": "1.15.1",
|
||||
"@octokit/graphql": "9.0.2",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@octokit/rest": "catalog:",
|
||||
"@openauthjs/openauth": "catalog:",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
@@ -246,7 +249,7 @@
|
||||
"@opentui/core": "0.0.0-20251211-4403a69a",
|
||||
"@opentui/solid": "0.0.0-20251211-4403a69a",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/precision-diffs": "catalog:",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
"@standard-schema/spec": "1.0.0",
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
@@ -305,7 +308,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.0.150",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
@@ -325,7 +328,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.0.150",
|
||||
"version": "1.0.163",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.88.1",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
@@ -336,7 +339,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.0.150",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -349,15 +352,18 @@
|
||||
},
|
||||
"packages/tauri": {
|
||||
"name": "@opencode-ai/tauri",
|
||||
"version": "1.0.150",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"@opencode-ai/desktop": "workspace:*",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-dialog": "~2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"@tauri-apps/plugin-os": "~2",
|
||||
"@tauri-apps/plugin-process": "~2",
|
||||
"@tauri-apps/plugin-shell": "~2",
|
||||
"@tauri-apps/plugin-store": "~2",
|
||||
"@tauri-apps/plugin-updater": "~2",
|
||||
"@tauri-apps/plugin-window-state": "~2",
|
||||
"solid-js": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -371,13 +377,15 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.0.150",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@pierre/precision-diffs": "catalog:",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@shikijs/transformers": "3.9.2",
|
||||
"@solid-primitives/bounds": "0.1.3",
|
||||
"@solid-primitives/resize-observer": "2.1.3",
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"fuzzysort": "catalog:",
|
||||
@@ -394,6 +402,7 @@
|
||||
"@tailwindcss/vite": "catalog:",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/luxon": "catalog:",
|
||||
"tailwindcss": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:",
|
||||
@@ -403,7 +412,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.0.150",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
@@ -414,7 +423,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.0.150",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
@@ -462,15 +471,16 @@
|
||||
"@cloudflare/workers-types": "4.20251008.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"@kobalte/core": "0.13.11",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@pierre/precision-diffs": "0.6.1",
|
||||
"@pierre/diffs": "1.0.0-beta.3",
|
||||
"@solidjs/meta": "0.29.4",
|
||||
"@solidjs/router": "0.15.4",
|
||||
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@tsconfig/node22": "22.0.2",
|
||||
"@types/bun": "1.3.3",
|
||||
"@types/bun": "1.3.4",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@types/node": "22.13.9",
|
||||
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|
||||
@@ -1088,11 +1098,11 @@
|
||||
|
||||
"@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
|
||||
|
||||
"@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@13.2.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-Tj4PkZyIL6eBMYcG/76QGsedF0+dWVeLhYprTmuFVVxzDW7PQh23tM0TP0z+1MvSkxB29YFZwnUX+cXfTiSdyw=="],
|
||||
"@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@14.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw=="],
|
||||
|
||||
"@octokit/plugin-request-log": ["@octokit/plugin-request-log@6.0.0", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q=="],
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.1.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-VztDkhM0ketQYSh5Im3IcKWFZl7VIrrsCaHbDINkdYeiiAsJzjhS2xRFCSJgfN6VOcsoW4laMtsmf3HcNqIimg=="],
|
||||
"@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@17.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw=="],
|
||||
|
||||
"@octokit/plugin-retry": ["@octokit/plugin-retry@3.0.9", "", { "dependencies": { "@octokit/types": "^6.0.3", "bottleneck": "^2.15.3" } }, "sha512-r+fArdP5+TG6l1Rv/C9hVoty6tldw6cE2pRHNGmFPdyfrc696R6JjrQ3d7HdVqGwuzfyrcaLAKD7K8TX8aehUQ=="],
|
||||
|
||||
@@ -1100,7 +1110,7 @@
|
||||
|
||||
"@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="],
|
||||
|
||||
"@octokit/rest": ["@octokit/rest@22.0.0", "", { "dependencies": { "@octokit/core": "^7.0.2", "@octokit/plugin-paginate-rest": "^13.0.1", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0" } }, "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA=="],
|
||||
"@octokit/rest": ["@octokit/rest@22.0.1", "", { "dependencies": { "@octokit/core": "^7.0.6", "@octokit/plugin-paginate-rest": "^14.0.0", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^17.0.0" } }, "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw=="],
|
||||
|
||||
"@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
|
||||
|
||||
@@ -1278,7 +1288,7 @@
|
||||
|
||||
"@petamoriken/float16": ["@petamoriken/float16@3.9.3", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="],
|
||||
|
||||
"@pierre/precision-diffs": ["@pierre/precision-diffs@0.6.1", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/transformers": "3.15.0", "diff": "8.0.2", "fast-deep-equal": "3.1.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "3.15.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-HXafRSOly6B0rRt6fuP0yy1MimHJMQ2NNnBGcIHhHwsgK4WWs+SBWRWt1usdgz0NIuSgXdIyQn8HY3F1jKyDBQ=="],
|
||||
"@pierre/diffs": ["@pierre/diffs@1.0.0-beta.3", "", { "dependencies": { "@shikijs/core": "3.19.0", "@shikijs/engine-javascript": "3.19.0", "@shikijs/transformers": "3.19.0", "diff": "8.0.2", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "3.19.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-W3dFWdFOBZ9OskGSOgN16aci8dsUyAavCxz3ZvbbVLTb2qRzMZ7H90qdfON13/N2l1HTyh84lkrCs1/sDvnRjQ=="],
|
||||
|
||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||
|
||||
@@ -1546,6 +1556,10 @@
|
||||
|
||||
"@solid-primitives/active-element": ["@solid-primitives/active-element@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9t5K4aR2naVDj950XU8OjnLgOg94a8k5wr6JNOPK+N5ESLsJDq42c1ZP8UKpewi1R+wplMMxiM6OPKRzbxJY7A=="],
|
||||
|
||||
"@solid-primitives/audio": ["@solid-primitives/audio@1.4.2", "", { "dependencies": { "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-UMD3ORQfI5Ky8yuKPxidDiEazsjv/dsoiKK5yZxLnsgaeNR1Aym3/77h/qT1jBYeXUgj4DX6t7NMpFUSVr14OQ=="],
|
||||
|
||||
"@solid-primitives/bounds": ["@solid-primitives/bounds@0.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/resize-observer": "^2.1.3", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-UbiyKMdSPmtijcEDnYLQL3zzaejpwWDAJJ4Gt5P0hgVs6A72piov0GyNw7V2SroH7NZFwxlYS22YmOr8A5xc1Q=="],
|
||||
|
||||
"@solid-primitives/event-bus": ["@solid-primitives/event-bus@1.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-l+n10/51neGcMaP3ypYt21bXfoeWh8IaC8k7fYuY3ww2a8S1Zv2N2a7FF5Qn+waTu86l0V8/nRHjkyqVIZBYwA=="],
|
||||
|
||||
"@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="],
|
||||
@@ -1658,12 +1672,18 @@
|
||||
|
||||
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew=="],
|
||||
|
||||
"@tauri-apps/plugin-os": ["@tauri-apps/plugin-os@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A=="],
|
||||
|
||||
"@tauri-apps/plugin-process": ["@tauri-apps/plugin-process@2.3.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA=="],
|
||||
|
||||
"@tauri-apps/plugin-shell": ["@tauri-apps/plugin-shell@2.3.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Xod+pRcFxmOWFWEnqH5yZcA7qwAMuaaDkMR1Sply+F8VfBj++CGnj2xf5UoialmjZ2Cvd8qrvSCbU+7GgNVsKQ=="],
|
||||
|
||||
"@tauri-apps/plugin-store": ["@tauri-apps/plugin-store@2.4.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ckGSEzZ5Ii4Hf2D5x25Oqnm2Zf9MfDWAzR+volY0z/OOBz6aucPKEY0F649JvQ0Vupku6UJo7ugpGRDOFOunkA=="],
|
||||
|
||||
"@tauri-apps/plugin-updater": ["@tauri-apps/plugin-updater@2.9.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg=="],
|
||||
|
||||
"@tauri-apps/plugin-window-state": ["@tauri-apps/plugin-window-state@2.4.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-OuvdrzyY8Q5Dbzpj+GcrnV1iCeoZbcFdzMjanZMMcAEUNy/6PH5pxZPXpaZLOR7whlzXiuzx0L9EKZbH7zpdRw=="],
|
||||
|
||||
"@thisbeyond/solid-dnd": ["@thisbeyond/solid-dnd@0.7.5", "", { "peerDependencies": { "solid-js": "^1.5" } }, "sha512-DfI5ff+yYGpK9M21LhYwIPlbP2msKxN2ARwuu6GF8tT1GgNVDTI8VCQvH4TJFoVApP9d44izmAcTh/iTCH2UUw=="],
|
||||
|
||||
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
||||
@@ -1686,7 +1706,7 @@
|
||||
|
||||
"@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
|
||||
"@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="],
|
||||
|
||||
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
||||
|
||||
@@ -1992,7 +2012,7 @@
|
||||
|
||||
"bun-pty": ["bun-pty@0.4.2", "", {}, "sha512-sHImDz6pJDsHAroYpC9ouKVgOyqZ7FP3N+stX5IdMddHve3rf9LIZBDomQcXrACQ7sQDNuwZQHG8BKR7w8krkQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
|
||||
"bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
|
||||
|
||||
"bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="],
|
||||
|
||||
@@ -4062,9 +4082,9 @@
|
||||
|
||||
"@octokit/oauth-methods/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="],
|
||||
|
||||
"@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="],
|
||||
"@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="],
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="],
|
||||
"@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="],
|
||||
|
||||
"@octokit/plugin-retry/@octokit/types": ["@octokit/types@6.41.0", "", { "dependencies": { "@octokit/openapi-types": "^12.11.0" } }, "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg=="],
|
||||
|
||||
@@ -4076,6 +4096,8 @@
|
||||
|
||||
"@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
|
||||
|
||||
"@opencode-ai/function/@octokit/rest": ["@octokit/rest@22.0.0", "", { "dependencies": { "@octokit/core": "^7.0.2", "@octokit/plugin-paginate-rest": "^13.0.1", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0" } }, "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA=="],
|
||||
|
||||
"@opencode-ai/tauri/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="],
|
||||
|
||||
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/types": "3.4.2" } }, "sha512-I5baLVi/ynLEOZoWSAMlACHNnG+yw5HDmse0oe+GW6U1u+ULdEB3UHiVWaHoJSSONV7tlcVxuaMy74sREDkSvg=="],
|
||||
@@ -4094,11 +4116,13 @@
|
||||
|
||||
"@parcel/watcher/node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
|
||||
|
||||
"@pierre/precision-diffs/@shikijs/core": ["@shikijs/core@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg=="],
|
||||
"@pierre/diffs/@shikijs/core": ["@shikijs/core@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-L7SrRibU7ZoYi1/TrZsJOFAnnHyLTE1SwHG1yNWjZIVCqjOEmCSuK2ZO9thnRbJG6TOkPp+Z963JmpCNw5nzvA=="],
|
||||
|
||||
"@pierre/precision-diffs/@shikijs/transformers": ["@shikijs/transformers@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/types": "3.15.0" } }, "sha512-Hmwip5ovvSkg+Kc41JTvSHHVfCYF+C8Cp1omb5AJj4Xvd+y9IXz2rKJwmFRGsuN0vpHxywcXJ1+Y4B9S7EG1/A=="],
|
||||
"@pierre/diffs/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-ZfWJNm2VMhKkQIKT9qXbs76RRcT0SF/CAvEz0+RkpUDAoDaCx0uFdCGzSRiD9gSlhm6AHkjdieOBJMaO2eC1rQ=="],
|
||||
|
||||
"@pierre/precision-diffs/shiki": ["shiki@3.15.0", "", { "dependencies": { "@shikijs/core": "3.15.0", "@shikijs/engine-javascript": "3.15.0", "@shikijs/engine-oniguruma": "3.15.0", "@shikijs/langs": "3.15.0", "@shikijs/themes": "3.15.0", "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw=="],
|
||||
"@pierre/diffs/@shikijs/transformers": ["@shikijs/transformers@3.19.0", "", { "dependencies": { "@shikijs/core": "3.19.0", "@shikijs/types": "3.19.0" } }, "sha512-e6vwrsyw+wx4OkcrDbL+FVCxwx8jgKiCoXzakVur++mIWVcgpzIi8vxf4/b4dVTYrV/nUx5RjinMf4tq8YV8Fw=="],
|
||||
|
||||
"@pierre/diffs/shiki": ["shiki@3.19.0", "", { "dependencies": { "@shikijs/core": "3.19.0", "@shikijs/engine-javascript": "3.19.0", "@shikijs/engine-oniguruma": "3.19.0", "@shikijs/langs": "3.19.0", "@shikijs/themes": "3.19.0", "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-77VJr3OR/VUZzPiStyRhADmO2jApMM0V2b1qf0RpfWya8Zr1PeZev5AEpPGAAKWdiYUtcZGBE4F5QvJml1PvWA=="],
|
||||
|
||||
"@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
|
||||
|
||||
@@ -4266,6 +4290,8 @@
|
||||
|
||||
"opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.27", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bpYruxVLhrTbVH6CCq48zMJNeHu6FmHtEedl9FXckEgcIEAi036idFhJlcRwC1jNCwlacbzb8dPD7OAH1EKJaQ=="],
|
||||
|
||||
"opencode/@octokit/rest": ["@octokit/rest@22.0.0", "", { "dependencies": { "@octokit/core": "^7.0.2", "@octokit/plugin-paginate-rest": "^13.0.1", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0" } }, "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
|
||||
|
||||
"opencontrol/@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="],
|
||||
@@ -4624,9 +4650,9 @@
|
||||
|
||||
"@octokit/oauth-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
|
||||
|
||||
"@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
|
||||
"@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
|
||||
"@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
|
||||
|
||||
"@octokit/plugin-retry/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@12.11.0", "", {}, "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ=="],
|
||||
|
||||
@@ -4634,6 +4660,10 @@
|
||||
|
||||
"@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
|
||||
|
||||
"@opencode-ai/function/@octokit/rest/@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@13.2.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-Tj4PkZyIL6eBMYcG/76QGsedF0+dWVeLhYprTmuFVVxzDW7PQh23tM0TP0z+1MvSkxB29YFZwnUX+cXfTiSdyw=="],
|
||||
|
||||
"@opencode-ai/function/@octokit/rest/@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.1.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-VztDkhM0ketQYSh5Im3IcKWFZl7VIrrsCaHbDINkdYeiiAsJzjhS2xRFCSJgfN6VOcsoW4laMtsmf3HcNqIimg=="],
|
||||
|
||||
"@opencode-ai/web/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AG8vnSi1W2pbgR2B911EfGqtLE9c4hQBYkv/x7Z+Kt0VxhgQKcW7UNDVYsu9YxwV6u+OJrvdJrMq6DNWoBjihQ=="],
|
||||
|
||||
"@opencode-ai/web/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.4.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg=="],
|
||||
@@ -4652,19 +4682,19 @@
|
||||
|
||||
"@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@pierre/precision-diffs/@shikijs/core/@shikijs/types": ["@shikijs/types@3.15.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw=="],
|
||||
"@pierre/diffs/@shikijs/core/@shikijs/types": ["@shikijs/types@3.19.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ=="],
|
||||
|
||||
"@pierre/precision-diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.15.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw=="],
|
||||
"@pierre/diffs/@shikijs/engine-javascript/@shikijs/types": ["@shikijs/types@3.19.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ=="],
|
||||
|
||||
"@pierre/precision-diffs/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg=="],
|
||||
"@pierre/diffs/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.19.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ=="],
|
||||
|
||||
"@pierre/precision-diffs/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-HnqFsV11skAHvOArMZdLBZZApRSYS4LSztk2K3016Y9VCyZISnlYUYsL2hzlS7tPqKHvNqmI5JSUJZprXloMvA=="],
|
||||
"@pierre/diffs/shiki/@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1hRxtYIJfJSZeM5ivbUXv9hcJP3PWRo5prG/V2sWwiubUKTa+7P62d2qxCW8jiVFX4pgRHhnHNp+qeR7Xl+6kg=="],
|
||||
|
||||
"@pierre/precision-diffs/shiki/@shikijs/langs": ["@shikijs/langs@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0" } }, "sha512-WpRvEFvkVvO65uKYW4Rzxs+IG0gToyM8SARQMtGGsH4GDMNZrr60qdggXrFOsdfOVssG/QQGEl3FnJ3EZ+8w8A=="],
|
||||
"@pierre/diffs/shiki/@shikijs/langs": ["@shikijs/langs@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0" } }, "sha512-dBMFzzg1QiXqCVQ5ONc0z2ebyoi5BKz+MtfByLm0o5/nbUu3Iz8uaTCa5uzGiscQKm7lVShfZHU1+OG3t5hgwg=="],
|
||||
|
||||
"@pierre/precision-diffs/shiki/@shikijs/themes": ["@shikijs/themes@3.15.0", "", { "dependencies": { "@shikijs/types": "3.15.0" } }, "sha512-8ow2zWb1IDvCKjYb0KiLNrK4offFdkfNVPXb1OZykpLCzRU6j+efkY+Y7VQjNlNFXonSw+4AOdGYtmqykDbRiQ=="],
|
||||
"@pierre/diffs/shiki/@shikijs/themes": ["@shikijs/themes@3.19.0", "", { "dependencies": { "@shikijs/types": "3.19.0" } }, "sha512-H36qw+oh91Y0s6OlFfdSuQ0Ld+5CgB/VE6gNPK+Hk4VRbVG/XQgkjnt4KzfnnoO6tZPtKJKHPjwebOCfjd6F8A=="],
|
||||
|
||||
"@pierre/precision-diffs/shiki/@shikijs/types": ["@shikijs/types@3.15.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw=="],
|
||||
"@pierre/diffs/shiki/@shikijs/types": ["@shikijs/types@3.19.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-Z2hdeEQlzuntf/BZpFG8a+Fsw9UVXdML7w0o3TgSXV3yNESGon+bs9ITkQb3Ki7zxoXOOu5oJWqZ2uto06V9iQ=="],
|
||||
|
||||
"@slack/web-api/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
@@ -4848,6 +4878,10 @@
|
||||
|
||||
"opencode/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
|
||||
|
||||
"opencode/@octokit/rest/@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@13.2.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-Tj4PkZyIL6eBMYcG/76QGsedF0+dWVeLhYprTmuFVVxzDW7PQh23tM0TP0z+1MvSkxB29YFZwnUX+cXfTiSdyw=="],
|
||||
|
||||
"opencode/@octokit/rest/@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.1.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-VztDkhM0ketQYSh5Im3IcKWFZl7VIrrsCaHbDINkdYeiiAsJzjhS2xRFCSJgfN6VOcsoW4laMtsmf3HcNqIimg=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="],
|
||||
@@ -5004,6 +5038,10 @@
|
||||
|
||||
"@modelcontextprotocol/sdk/raw-body/http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"@opencode-ai/function/@octokit/rest/@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="],
|
||||
|
||||
"@opencode-ai/function/@octokit/rest/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="],
|
||||
|
||||
"@slack/web-api/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
|
||||
@@ -5024,6 +5062,10 @@
|
||||
|
||||
"js-beautify/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"opencode/@octokit/rest/@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="],
|
||||
|
||||
"opencode/@octokit/rest/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
|
||||
@@ -5118,10 +5160,18 @@
|
||||
|
||||
"@jsx-email/cli/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"@opencode-ai/function/@octokit/rest/@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
|
||||
|
||||
"@opencode-ai/function/@octokit/rest/@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
|
||||
|
||||
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="],
|
||||
|
||||
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex-recursion": ["regex-recursion@5.1.1", "", { "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w=="],
|
||||
|
||||
"opencode/@octokit/rest/@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
|
||||
|
||||
"opencode/@octokit/rest/@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"opencontrol/@modelcontextprotocol/sdk/express/body-parser/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
6
flake.lock
generated
6
flake.lock
generated
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1765270179,
|
||||
"narHash": "sha256-g2a4MhRKu4ymR4xwo+I+auTknXt/+j37Lnf0Mvfl1rE=",
|
||||
"lastModified": 1765803225,
|
||||
"narHash": "sha256-xwaZV/UgJ04+ixbZZfoDE8IsOWjtvQZICh9aamzPnrg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "677fbe97984e7af3175b6c121f3c39ee5c8d62c9",
|
||||
"rev": "ac9a217389ee622d4e1e727c4efcc9c4bc9089ba",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -17,6 +17,11 @@ inputs:
|
||||
description: "Custom prompt to override the default prompt"
|
||||
required: false
|
||||
|
||||
use_github_token:
|
||||
description: "Use GITHUB_TOKEN directly instead of OpenCode App token exchange. When true, skips OIDC and uses the GITHUB_TOKEN env var."
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
@@ -51,3 +56,4 @@ runs:
|
||||
MODEL: ${{ inputs.model }}
|
||||
SHARE: ${{ inputs.share }}
|
||||
PROMPT: ${{ inputs.prompt }}
|
||||
USE_GITHUB_TOKEN: ${{ inputs.use_github_token }}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"@actions/core": "1.11.1",
|
||||
"@actions/github": "6.0.1",
|
||||
"@octokit/graphql": "9.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@octokit/rest": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,7 @@ const ZEN_MODELS = [
|
||||
new sst.Secret("ZEN_MODELS2"),
|
||||
new sst.Secret("ZEN_MODELS3"),
|
||||
new sst.Secret("ZEN_MODELS4"),
|
||||
new sst.Secret("ZEN_MODELS5"),
|
||||
]
|
||||
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
||||
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
||||
|
||||
17
install
17
install
@@ -240,22 +240,23 @@ download_with_progress() {
|
||||
|
||||
download_and_install() {
|
||||
print_message info "\n${MUTED}Installing ${NC}opencode ${MUTED}version: ${NC}$specific_version"
|
||||
mkdir -p opencodetmp && cd opencodetmp
|
||||
local tmp_dir="${TMPDIR:-/tmp}/opencode_install_$$"
|
||||
mkdir -p "$tmp_dir"
|
||||
|
||||
if [[ "$os" == "windows" ]] || ! download_with_progress "$url" "$filename"; then
|
||||
# Fallback to standard curl on Windows or if custom progress fails
|
||||
curl -# -L -o "$filename" "$url"
|
||||
if [[ "$os" == "windows" ]] || ! [ -t 2 ] || ! download_with_progress "$url" "$tmp_dir/$filename"; then
|
||||
# Fallback to standard curl on Windows, non-TTY environments, or if custom progress fails
|
||||
curl -# -L -o "$tmp_dir/$filename" "$url"
|
||||
fi
|
||||
|
||||
if [ "$os" = "linux" ]; then
|
||||
tar -xzf "$filename"
|
||||
tar -xzf "$tmp_dir/$filename" -C "$tmp_dir"
|
||||
else
|
||||
unzip -q "$filename"
|
||||
unzip -q "$tmp_dir/$filename" -d "$tmp_dir"
|
||||
fi
|
||||
|
||||
mv opencode "$INSTALL_DIR"
|
||||
mv "$tmp_dir/opencode" "$INSTALL_DIR"
|
||||
chmod 755 "${INSTALL_DIR}/opencode"
|
||||
cd .. && rm -rf opencodetmp
|
||||
rm -rf "$tmp_dir"
|
||||
}
|
||||
|
||||
check_version
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"nodeModules": "sha256-3GaqUwomnIUW8MqUi1jDVPHQ/C5Z+D9wMR//tAGxvSQ="
|
||||
"nodeModules": "sha256-IkvFO/dANwC8MCOW8PqILqyxCa4IDiFZIIM3B4GMB+Q="
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "AI-powered development tool",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.3.3",
|
||||
"packageManager": "bun@1.3.4",
|
||||
"scripts": {
|
||||
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
|
||||
"typecheck": "bun turbo typecheck",
|
||||
@@ -20,7 +20,8 @@
|
||||
"packages/slack"
|
||||
],
|
||||
"catalog": {
|
||||
"@types/bun": "1.3.3",
|
||||
"@types/bun": "1.3.4",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@hono/zod-validator": "0.4.2",
|
||||
"ulid": "3.0.1",
|
||||
"@kobalte/core": "0.13.11",
|
||||
@@ -30,7 +31,7 @@
|
||||
"@tsconfig/bun": "1.0.9",
|
||||
"@cloudflare/workers-types": "4.20251008.0",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"@pierre/precision-diffs": "0.6.1",
|
||||
"@pierre/diffs": "1.0.0-beta.3",
|
||||
"@tailwindcss/vite": "4.1.11",
|
||||
"diff": "8.0.2",
|
||||
"ai": "5.0.97",
|
||||
@@ -62,6 +63,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.933.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"typescript": "catalog:"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.0.150",
|
||||
"version": "1.0.163",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
||||
@@ -119,8 +119,8 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) {
|
||||
<section data-component="top">
|
||||
<div onContextMenu={handleLogoContextMenu}>
|
||||
<A href="/">
|
||||
<img data-slot="logo light" src={logoLight} alt="opencode logo light" />
|
||||
<img data-slot="logo dark" src={logoDark} alt="opencode logo dark" />
|
||||
<img data-slot="logo light" src={logoLight} alt="opencode logo light" width="189" height="34" />
|
||||
<img data-slot="logo dark" src={logoDark} alt="opencode logo dark" width="189" height="34" />
|
||||
</A>
|
||||
</div>
|
||||
|
||||
@@ -169,6 +169,25 @@ export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) {
|
||||
</Match>
|
||||
</Switch>
|
||||
</li>
|
||||
<Show when={!props.hideGetStarted}>
|
||||
{" "}
|
||||
<li>
|
||||
{" "}
|
||||
<A href="/download" data-slot="cta-button">
|
||||
{" "}
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{" "}
|
||||
<path
|
||||
d="M12.1875 9.75L9.00001 12.9375L5.8125 9.75M9.00001 2.0625L9 12.375M14.4375 15.9375H3.5625"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="square"
|
||||
/>{" "}
|
||||
</svg>{" "}
|
||||
Free{" "}
|
||||
</A>{" "}
|
||||
</li>
|
||||
</Show>
|
||||
</ul>
|
||||
</nav>
|
||||
<nav data-component="nav-mobile">
|
||||
|
||||
@@ -22,8 +22,8 @@ export const config = {
|
||||
|
||||
// Static stats (used on landing page)
|
||||
stats: {
|
||||
contributors: "375",
|
||||
commits: "5,250",
|
||||
contributors: "400",
|
||||
commits: "5,000",
|
||||
monthlyUsers: "400,000",
|
||||
},
|
||||
} as const
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// @refresh reload
|
||||
import { createHandler, StartServer } from "@solidjs/start/server"
|
||||
|
||||
const criticalCSS = `[data-component="top"]{min-height:80px;display:flex;align-items:center}`
|
||||
|
||||
export default createHandler(
|
||||
() => (
|
||||
<StartServer
|
||||
@@ -11,6 +13,7 @@ export default createHandler(
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta property="og:image" content="/social-share.png" />
|
||||
<meta property="twitter:image" content="/social-share.png" />
|
||||
<style>{criticalCSS}</style>
|
||||
{assets}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -26,6 +26,7 @@ export const github = query(async () => {
|
||||
release: {
|
||||
name: release.name,
|
||||
url: release.html_url,
|
||||
tag_name: release.tag_name,
|
||||
},
|
||||
contributors: contributorCount,
|
||||
}
|
||||
|
||||
@@ -9,13 +9,6 @@ import desktopAppIcon from "../../asset/lander/opencode-desktop-icon.png"
|
||||
import { Legal } from "~/component/legal"
|
||||
import { config } from "~/config"
|
||||
|
||||
const getLatestRelease = query(async () => {
|
||||
const response = await fetch("https://api.github.com/repos/sst/opencode/releases/latest")
|
||||
if (!response.ok) return null
|
||||
const data = await response.json()
|
||||
return data.tag_name as string
|
||||
}, "latest-release")
|
||||
|
||||
function CopyStatus() {
|
||||
return (
|
||||
<span data-component="copy-status">
|
||||
@@ -26,14 +19,7 @@ function CopyStatus() {
|
||||
}
|
||||
|
||||
export default function Download() {
|
||||
const release = createAsync(() => getLatestRelease(), {
|
||||
deferStream: true,
|
||||
})
|
||||
const download = () => {
|
||||
const version = release()
|
||||
if (!version) return null
|
||||
return `https://github.com/sst/opencode/releases/download/${version}`
|
||||
}
|
||||
const downloadUrl = "https://github.com/sst/opencode/releases/latest/download"
|
||||
const handleCopyClick = (command: string) => (event: Event) => {
|
||||
const button = event.currentTarget as HTMLButtonElement
|
||||
navigator.clipboard.writeText(command)
|
||||
@@ -107,6 +93,12 @@ export default function Download() {
|
||||
<span>[2]</span> OpenCode Desktop (Beta)
|
||||
</div>
|
||||
<div data-component="section-content">
|
||||
<button data-component="cli-row" onClick={handleCopyClick("brew install --cask opencode-desktop")}>
|
||||
<code>
|
||||
brew install --cask <strong>opencode-desktop</strong>
|
||||
</code>
|
||||
<CopyStatus />
|
||||
</button>
|
||||
<div data-component="download-row">
|
||||
<div data-component="download-info">
|
||||
<span data-slot="icon">
|
||||
@@ -121,7 +113,7 @@ export default function Download() {
|
||||
macOS (<span data-slot="hide-narrow">Apple </span>Silicon)
|
||||
</span>
|
||||
</div>
|
||||
<a href={download() + "/opencode-desktop-darwin-aarch64.dmg"} data-component="action-button">
|
||||
<a href={downloadUrl + "/opencode-desktop-darwin-aarch64.dmg"} data-component="action-button">
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
@@ -137,7 +129,7 @@ export default function Download() {
|
||||
</span>
|
||||
<span>macOS (Intel)</span>
|
||||
</div>
|
||||
<a href={download() + "/opencode-desktop-darwin-x64.dmg"} data-component="action-button">
|
||||
<a href={downloadUrl + "/opencode-desktop-darwin-x64.dmg"} data-component="action-button">
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
@@ -160,7 +152,7 @@ export default function Download() {
|
||||
</span>
|
||||
<span>Windows (x64)</span>
|
||||
</div>
|
||||
<a href={download() + "/opencode-desktop-windows-x64.exe"} data-component="action-button">
|
||||
<a href={downloadUrl + "/opencode-desktop-windows-x64.exe"} data-component="action-button">
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
@@ -176,7 +168,7 @@ export default function Download() {
|
||||
</span>
|
||||
<span>Linux (.deb)</span>
|
||||
</div>
|
||||
<a href={download() + "/opencode-desktop-linux-amd64.deb"} data-component="action-button">
|
||||
<a href={downloadUrl + "/opencode-desktop-linux-amd64.deb"} data-component="action-button">
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
@@ -192,7 +184,7 @@ export default function Download() {
|
||||
</span>
|
||||
<span>Linux (.rpm)</span>
|
||||
</div>
|
||||
<a href={download() + "/opencode-desktop-linux-x86_64.rpm"} data-component="action-button">
|
||||
<a href={downloadUrl + "/opencode-desktop-linux-x86_64.rpm"} data-component="action-button">
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -206,6 +206,7 @@ body {
|
||||
[data-component="top"] {
|
||||
padding: 24px var(--padding);
|
||||
height: 80px;
|
||||
min-height: 80px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
display: flex;
|
||||
|
||||
@@ -52,6 +52,21 @@ export default function Home() {
|
||||
|
||||
<div data-component="content">
|
||||
<section data-component="hero">
|
||||
<div data-component="desktop-app-banner">
|
||||
<span data-slot="badge">New</span>
|
||||
<div data-slot="content">
|
||||
<span data-slot="text">
|
||||
Desktop app available in beta<span data-slot="platforms"> on macOS, Windows, and Linux</span>.
|
||||
</span>
|
||||
<a href="/download" data-slot="link">
|
||||
Download now
|
||||
</a>
|
||||
<a href="/download" data-slot="link-mobile">
|
||||
Download the desktop beta now
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-slot="hero-copy">
|
||||
{/*<a data-slot="releases"*/}
|
||||
{/* href={release()?.url ?? `${config.github.repoUrl}/releases`}*/}
|
||||
@@ -213,7 +228,7 @@ export default function Home() {
|
||||
<span>[*]</span>
|
||||
<p>
|
||||
With over <strong>{config.github.starsFormatted.full}</strong> GitHub stars,{" "}
|
||||
<strong>{config.stats.contributors}</strong> contributors, and almost{" "}
|
||||
<strong>{config.stats.contributors}</strong> contributors, and over{" "}
|
||||
<strong>{config.stats.commits}</strong> commits, OpenCode is used and trusted by over{" "}
|
||||
<strong>{config.stats.monthlyUsers}</strong> developers every month.
|
||||
</p>
|
||||
|
||||
@@ -43,9 +43,12 @@ const getModelsInfo = query(async (workspaceID: string) => {
|
||||
const pA = getPriority(idA)
|
||||
const pB = getPriority(idB)
|
||||
if (pA !== pB) return pA - pB
|
||||
return modelA.name.localeCompare(modelB.name)
|
||||
|
||||
const modelAName = Array.isArray(modelA) ? modelA[0].name : modelA.name
|
||||
const modelBName = Array.isArray(modelB) ? modelB[0].name : modelB.name
|
||||
return modelAName.localeCompare(modelBName)
|
||||
})
|
||||
.map(([id, model]) => ({ id, name: model.name })),
|
||||
.map(([id, model]) => ({ id, name: Array.isArray(model) ? model[0].name : model.name })),
|
||||
disabled: await Model.listDisabled(),
|
||||
}
|
||||
}, workspaceID)
|
||||
|
||||
@@ -57,15 +57,17 @@ export async function handler(
|
||||
const sessionId = input.request.headers.get("x-opencode-session") ?? ""
|
||||
const requestId = input.request.headers.get("x-opencode-request") ?? ""
|
||||
const projectId = input.request.headers.get("x-opencode-project") ?? ""
|
||||
const ocClient = input.request.headers.get("x-opencode-client") ?? ""
|
||||
logger.metric({
|
||||
is_tream: isStream,
|
||||
session: sessionId,
|
||||
request: requestId,
|
||||
client: ocClient,
|
||||
})
|
||||
const zenData = ZenData.list()
|
||||
const modelInfo = validateModel(zenData, model)
|
||||
const dataDumper = createDataDumper(sessionId, requestId, projectId)
|
||||
const trialLimiter = createTrialLimiter(modelInfo.trial?.limit, ip)
|
||||
const trialLimiter = createTrialLimiter(modelInfo.trial, ip, ocClient)
|
||||
const isTrial = await trialLimiter?.isTrial()
|
||||
const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip)
|
||||
await rateLimiter?.check()
|
||||
@@ -286,11 +288,14 @@ export async function handler(
|
||||
}
|
||||
|
||||
function validateModel(zenData: ZenData, reqModel: string) {
|
||||
if (!(reqModel in zenData.models)) {
|
||||
throw new ModelError(`Model ${reqModel} not supported`)
|
||||
}
|
||||
if (!(reqModel in zenData.models)) throw new ModelError(`Model ${reqModel} not supported`)
|
||||
|
||||
const modelId = reqModel as keyof typeof zenData.models
|
||||
const modelData = zenData.models[modelId]
|
||||
const modelData = Array.isArray(zenData.models[modelId])
|
||||
? zenData.models[modelId].find((model) => opts.format === model.formatFilter)
|
||||
: zenData.models[modelId]
|
||||
|
||||
if (!modelData) throw new ModelError(`Model ${reqModel} not supported for format ${opts.format}`)
|
||||
|
||||
logger.metric({ model: modelId })
|
||||
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { Database, eq, sql } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { IpTable } from "@opencode-ai/console-core/schema/ip.sql.js"
|
||||
import { UsageInfo } from "./provider/provider"
|
||||
import { ZenData } from "@opencode-ai/console-core/model.js"
|
||||
|
||||
export function createTrialLimiter(limit: number | undefined, ip: string) {
|
||||
if (!limit) return
|
||||
export function createTrialLimiter(trial: ZenData.Trial | undefined, ip: string, client: string) {
|
||||
if (!trial) return
|
||||
if (!ip) return
|
||||
|
||||
let trial: boolean
|
||||
const limit =
|
||||
trial.limits.find((limit) => limit.client === client)?.limit ??
|
||||
trial.limits.find((limit) => limit.client === undefined)?.limit
|
||||
if (!limit) return
|
||||
|
||||
let _isTrial: boolean
|
||||
|
||||
return {
|
||||
isTrial: async () => {
|
||||
@@ -20,11 +26,11 @@ export function createTrialLimiter(limit: number | undefined, ip: string) {
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
|
||||
trial = (data?.usage ?? 0) < limit
|
||||
return trial
|
||||
_isTrial = (data?.usage ?? 0) < limit
|
||||
return _isTrial
|
||||
},
|
||||
track: async (usageInfo: UsageInfo) => {
|
||||
if (!trial) return
|
||||
if (!_isTrial) return
|
||||
const usage =
|
||||
usageInfo.inputTokens +
|
||||
usageInfo.outputTokens +
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.0.150",
|
||||
"version": "1.0.163",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -16,16 +16,19 @@ const value1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[
|
||||
const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
|
||||
const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
|
||||
const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
||||
const value5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
|
||||
if (!value1) throw new Error("ZEN_MODELS1 not found")
|
||||
if (!value2) throw new Error("ZEN_MODELS2 not found")
|
||||
if (!value3) throw new Error("ZEN_MODELS3 not found")
|
||||
if (!value4) throw new Error("ZEN_MODELS4 not found")
|
||||
if (!value5) throw new Error("ZEN_MODELS5 not found")
|
||||
|
||||
// validate value
|
||||
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4))
|
||||
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5))
|
||||
|
||||
// update the secret
|
||||
await $`bun sst secret set ZEN_MODELS1 ${value1} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS2 ${value2} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS3 ${value3} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS4 ${value4} --stage ${stage}`
|
||||
await $`bun sst secret set ZEN_MODELS5 ${value5} --stage ${stage}`
|
||||
|
||||
@@ -16,16 +16,19 @@ const value1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[
|
||||
const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
|
||||
const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
|
||||
const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
||||
const value5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
|
||||
if (!value1) throw new Error("ZEN_MODELS1 not found")
|
||||
if (!value2) throw new Error("ZEN_MODELS2 not found")
|
||||
if (!value3) throw new Error("ZEN_MODELS3 not found")
|
||||
if (!value4) throw new Error("ZEN_MODELS4 not found")
|
||||
if (!value5) throw new Error("ZEN_MODELS5 not found")
|
||||
|
||||
// validate value
|
||||
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4))
|
||||
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5))
|
||||
|
||||
// update the secret
|
||||
await $`bun sst secret set ZEN_MODELS1 ${value1}`
|
||||
await $`bun sst secret set ZEN_MODELS2 ${value2}`
|
||||
await $`bun sst secret set ZEN_MODELS3 ${value3}`
|
||||
await $`bun sst secret set ZEN_MODELS4 ${value4}`
|
||||
await $`bun sst secret set ZEN_MODELS5 ${value5}`
|
||||
|
||||
@@ -14,15 +14,17 @@ const oldValue1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=
|
||||
const oldValue2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
|
||||
const oldValue3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
|
||||
const oldValue4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
|
||||
const oldValue5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
|
||||
if (!oldValue1) throw new Error("ZEN_MODELS1 not found")
|
||||
if (!oldValue2) throw new Error("ZEN_MODELS2 not found")
|
||||
if (!oldValue3) throw new Error("ZEN_MODELS3 not found")
|
||||
if (!oldValue4) throw new Error("ZEN_MODELS4 not found")
|
||||
if (!oldValue5) throw new Error("ZEN_MODELS5 not found")
|
||||
|
||||
// store the prettified json to a temp file
|
||||
const filename = `models-${Date.now()}.json`
|
||||
const tempFile = Bun.file(path.join(os.tmpdir(), filename))
|
||||
await tempFile.write(JSON.stringify(JSON.parse(oldValue1 + oldValue2 + oldValue3 + oldValue4), null, 2))
|
||||
await tempFile.write(JSON.stringify(JSON.parse(oldValue1 + oldValue2 + oldValue3 + oldValue4 + oldValue5), null, 2))
|
||||
console.log("tempFile", tempFile.name)
|
||||
|
||||
// open temp file in vim and read the file on close
|
||||
@@ -31,12 +33,15 @@ const newValue = JSON.stringify(JSON.parse(await tempFile.text()))
|
||||
ZenData.validate(JSON.parse(newValue))
|
||||
|
||||
// update the secret
|
||||
const chunk = Math.ceil(newValue.length / 4)
|
||||
const chunk = Math.ceil(newValue.length / 5)
|
||||
const newValue1 = newValue.slice(0, chunk)
|
||||
const newValue2 = newValue.slice(chunk, chunk * 2)
|
||||
const newValue3 = newValue.slice(chunk * 2, chunk * 3)
|
||||
const newValue4 = newValue.slice(chunk * 3)
|
||||
const newValue4 = newValue.slice(chunk * 3, chunk * 4)
|
||||
const newValue5 = newValue.slice(chunk * 4)
|
||||
|
||||
await $`bun sst secret set ZEN_MODELS1 ${newValue1}`
|
||||
await $`bun sst secret set ZEN_MODELS2 ${newValue2}`
|
||||
await $`bun sst secret set ZEN_MODELS3 ${newValue3}`
|
||||
await $`bun sst secret set ZEN_MODELS4 ${newValue4}`
|
||||
await $`bun sst secret set ZEN_MODELS5 ${newValue5}`
|
||||
|
||||
@@ -9,7 +9,17 @@ import { Resource } from "@opencode-ai/console-resource"
|
||||
|
||||
export namespace ZenData {
|
||||
const FormatSchema = z.enum(["anthropic", "google", "openai", "oa-compat"])
|
||||
const TrialSchema = z.object({
|
||||
provider: z.string(),
|
||||
limits: z.array(
|
||||
z.object({
|
||||
limit: z.number(),
|
||||
client: z.enum(["cli", "desktop"]).optional(),
|
||||
}),
|
||||
),
|
||||
})
|
||||
export type Format = z.infer<typeof FormatSchema>
|
||||
export type Trial = z.infer<typeof TrialSchema>
|
||||
|
||||
const ModelCostSchema = z.object({
|
||||
input: z.number(),
|
||||
@@ -26,12 +36,7 @@ export namespace ZenData {
|
||||
allowAnonymous: z.boolean().optional(),
|
||||
byokProvider: z.enum(["openai", "anthropic", "google"]).optional(),
|
||||
stickyProvider: z.boolean().optional(),
|
||||
trial: z
|
||||
.object({
|
||||
limit: z.number(),
|
||||
provider: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
trial: TrialSchema.optional(),
|
||||
rateLimit: z.number().optional(),
|
||||
fallbackProvider: z.string().optional(),
|
||||
providers: z.array(
|
||||
@@ -53,7 +58,7 @@ export namespace ZenData {
|
||||
})
|
||||
|
||||
const ModelsSchema = z.object({
|
||||
models: z.record(z.string(), ModelSchema),
|
||||
models: z.record(z.string(), z.union([ModelSchema, z.array(ModelSchema.extend({ formatFilter: FormatSchema }))])),
|
||||
providers: z.record(z.string(), ProviderSchema),
|
||||
})
|
||||
|
||||
@@ -63,7 +68,11 @@ export namespace ZenData {
|
||||
|
||||
export const list = fn(z.void(), () => {
|
||||
const json = JSON.parse(
|
||||
Resource.ZEN_MODELS1.value + Resource.ZEN_MODELS2.value + Resource.ZEN_MODELS3.value + Resource.ZEN_MODELS4.value,
|
||||
Resource.ZEN_MODELS1.value +
|
||||
Resource.ZEN_MODELS2.value +
|
||||
Resource.ZEN_MODELS3.value +
|
||||
Resource.ZEN_MODELS4.value +
|
||||
Resource.ZEN_MODELS5.value,
|
||||
)
|
||||
return ModelsSchema.parse(json)
|
||||
})
|
||||
|
||||
12
packages/console/core/sst-env.d.ts
vendored
12
packages/console/core/sst-env.d.ts
vendored
@@ -50,10 +50,6 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"Enterprise": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
"GITHUB_APP_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
@@ -94,6 +90,10 @@ declare module "sst" {
|
||||
"type": "sst.sst.Linkable"
|
||||
"value": string
|
||||
}
|
||||
"Teams": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
"Web": {
|
||||
"type": "sst.cloudflare.Astro"
|
||||
"url": string
|
||||
@@ -114,6 +114,10 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS5": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
}
|
||||
}
|
||||
// cloudflare
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.0.150",
|
||||
"version": "1.0.163",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
12
packages/console/function/sst-env.d.ts
vendored
12
packages/console/function/sst-env.d.ts
vendored
@@ -50,10 +50,6 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"Enterprise": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
"GITHUB_APP_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
@@ -94,6 +90,10 @@ declare module "sst" {
|
||||
"type": "sst.sst.Linkable"
|
||||
"value": string
|
||||
}
|
||||
"Teams": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
"Web": {
|
||||
"type": "sst.cloudflare.Astro"
|
||||
"url": string
|
||||
@@ -114,6 +114,10 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS5": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
}
|
||||
}
|
||||
// cloudflare
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.0.150",
|
||||
"version": "1.0.163",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
12
packages/console/resource/sst-env.d.ts
vendored
12
packages/console/resource/sst-env.d.ts
vendored
@@ -50,10 +50,6 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"Enterprise": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
"GITHUB_APP_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
@@ -94,6 +90,10 @@ declare module "sst" {
|
||||
"type": "sst.sst.Linkable"
|
||||
"value": string
|
||||
}
|
||||
"Teams": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
"Web": {
|
||||
"type": "sst.cloudflare.Astro"
|
||||
"url": string
|
||||
@@ -114,6 +114,10 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS5": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
}
|
||||
}
|
||||
// cloudflare
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<meta property="og:image" content="/social-share.png" />
|
||||
<meta property="twitter:image" content="/social-share.png" />
|
||||
</head>
|
||||
<body class="antialiased overscroll-none select-none text-12-regular">
|
||||
<body class="antialiased overscroll-none select-none text-12-regular overflow-hidden">
|
||||
<script>
|
||||
;(function () {
|
||||
const savedTheme = localStorage.getItem("theme") || "oc-1"
|
||||
@@ -22,7 +22,7 @@
|
||||
})()
|
||||
</script>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<div id="root" class="flex flex-col h-screen"></div>
|
||||
<script src="/src/entry.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.0.150",
|
||||
"version": "1.0.163",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -35,7 +35,9 @@
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@shikijs/transformers": "3.9.2",
|
||||
"@solid-primitives/active-element": "2.1.3",
|
||||
"@solid-primitives/audio": "1.4.2",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
"@solid-primitives/media": "2.3.3",
|
||||
"@solid-primitives/resize-observer": "2.1.3",
|
||||
"@solid-primitives/scroll": "2.1.3",
|
||||
"@solid-primitives/storage": "4.3.3",
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
import "@/index.css"
|
||||
import { Show } from "solid-js"
|
||||
import { Router, Route, Navigate } from "@solidjs/router"
|
||||
import { MetaProvider } from "@solidjs/meta"
|
||||
import { Font } from "@opencode-ai/ui/font"
|
||||
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
|
||||
import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
|
||||
import { CodeComponentProvider } from "@opencode-ai/ui/context/code"
|
||||
import { Diff } from "@opencode-ai/ui/diff"
|
||||
import { GlobalSyncProvider } from "./context/global-sync"
|
||||
import { Code } from "@opencode-ai/ui/code"
|
||||
import { GlobalSyncProvider } from "@/context/global-sync"
|
||||
import { LayoutProvider } from "@/context/layout"
|
||||
import { GlobalSDKProvider } from "@/context/global-sdk"
|
||||
import { TerminalProvider } from "@/context/terminal"
|
||||
import { PromptProvider } from "@/context/prompt"
|
||||
import { NotificationProvider } from "@/context/notification"
|
||||
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
|
||||
import { CommandProvider } from "@/context/command"
|
||||
import Layout from "@/pages/layout"
|
||||
import Home from "@/pages/home"
|
||||
import DirectoryLayout from "@/pages/directory-layout"
|
||||
import Session from "@/pages/session"
|
||||
import { LayoutProvider } from "./context/layout"
|
||||
import { GlobalSDKProvider } from "./context/global-sdk"
|
||||
import { SessionProvider } from "./context/session"
|
||||
import { Show } from "solid-js"
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -32,34 +38,48 @@ const url =
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<MarkedProvider>
|
||||
<DiffComponentProvider component={Diff}>
|
||||
<GlobalSDKProvider url={url}>
|
||||
<GlobalSyncProvider>
|
||||
<LayoutProvider>
|
||||
<MetaProvider>
|
||||
<Font />
|
||||
<Router root={Layout}>
|
||||
<Route path="/" component={Home} />
|
||||
<Route path="/:dir" component={DirectoryLayout}>
|
||||
<Route path="/" component={() => <Navigate href="session" />} />
|
||||
<Route
|
||||
path="/session/:id?"
|
||||
component={(p) => (
|
||||
<Show when={p.params.id || true} keyed>
|
||||
<SessionProvider>
|
||||
<Session />
|
||||
</SessionProvider>
|
||||
</Show>
|
||||
)}
|
||||
/>
|
||||
</Route>
|
||||
</Router>
|
||||
</MetaProvider>
|
||||
</LayoutProvider>
|
||||
</GlobalSyncProvider>
|
||||
</GlobalSDKProvider>
|
||||
</DiffComponentProvider>
|
||||
</MarkedProvider>
|
||||
<DialogProvider>
|
||||
<MarkedProvider>
|
||||
<DiffComponentProvider component={Diff}>
|
||||
<CodeComponentProvider component={Code}>
|
||||
<GlobalSDKProvider url={url}>
|
||||
<GlobalSyncProvider>
|
||||
<LayoutProvider>
|
||||
<NotificationProvider>
|
||||
<MetaProvider>
|
||||
<Font />
|
||||
<Router
|
||||
root={(props) => (
|
||||
<CommandProvider>
|
||||
<Layout>{props.children}</Layout>
|
||||
</CommandProvider>
|
||||
)}
|
||||
>
|
||||
<Route path="/" component={Home} />
|
||||
<Route path="/:dir" component={DirectoryLayout}>
|
||||
<Route path="/" component={() => <Navigate href="session" />} />
|
||||
<Route
|
||||
path="/session/:id?"
|
||||
component={(p) => (
|
||||
<Show when={p.params.id || true} keyed>
|
||||
<TerminalProvider>
|
||||
<PromptProvider>
|
||||
<Session />
|
||||
</PromptProvider>
|
||||
</TerminalProvider>
|
||||
</Show>
|
||||
)}
|
||||
/>
|
||||
</Route>
|
||||
</Router>
|
||||
</MetaProvider>
|
||||
</NotificationProvider>
|
||||
</LayoutProvider>
|
||||
</GlobalSyncProvider>
|
||||
</GlobalSDKProvider>
|
||||
</CodeComponentProvider>
|
||||
</DiffComponentProvider>
|
||||
</MarkedProvider>
|
||||
</DialogProvider>
|
||||
)
|
||||
}
|
||||
|
||||
381
packages/desktop/src/components/dialog-connect-provider.tsx
Normal file
381
packages/desktop/src/components/dialog-connect-provider.tsx
Normal file
@@ -0,0 +1,381 @@
|
||||
import { createMemo, Match, onCleanup, onMount, Switch } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
import { ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List, ListRef } from "@opencode-ai/ui/list"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { iife } from "@opencode-ai/util/iife"
|
||||
import { Link } from "@/components/link"
|
||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||
import { DialogSelectModel } from "./dialog-select-model"
|
||||
|
||||
export function DialogConnectProvider(props: { provider: string }) {
|
||||
const dialog = useDialog()
|
||||
const globalSync = useGlobalSync()
|
||||
const globalSDK = useGlobalSDK()
|
||||
const platform = usePlatform()
|
||||
const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!)
|
||||
const methods = createMemo(
|
||||
() =>
|
||||
globalSync.data.provider_auth[props.provider] ?? [
|
||||
{
|
||||
type: "api",
|
||||
label: "API key",
|
||||
},
|
||||
],
|
||||
)
|
||||
const [store, setStore] = createStore({
|
||||
methodIndex: undefined as undefined | number,
|
||||
authorization: undefined as undefined | ProviderAuthAuthorization,
|
||||
state: "pending" as undefined | "pending" | "complete" | "error",
|
||||
error: undefined as string | undefined,
|
||||
})
|
||||
|
||||
const method = createMemo(() => (store.methodIndex !== undefined ? methods().at(store.methodIndex!) : undefined))
|
||||
|
||||
async function selectMethod(index: number) {
|
||||
const method = methods()[index]
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
draft.methodIndex = index
|
||||
draft.authorization = undefined
|
||||
draft.state = undefined
|
||||
draft.error = undefined
|
||||
}),
|
||||
)
|
||||
|
||||
if (method.type === "oauth") {
|
||||
setStore("state", "pending")
|
||||
const start = Date.now()
|
||||
await globalSDK.client.provider.oauth
|
||||
.authorize(
|
||||
{
|
||||
providerID: props.provider,
|
||||
method: index,
|
||||
},
|
||||
{ throwOnError: true },
|
||||
)
|
||||
.then((x) => {
|
||||
const elapsed = Date.now() - start
|
||||
const delay = 1000 - elapsed
|
||||
|
||||
if (delay > 0) {
|
||||
setTimeout(() => {
|
||||
setStore("state", "complete")
|
||||
setStore("authorization", x.data!)
|
||||
}, delay)
|
||||
return
|
||||
}
|
||||
setStore("state", "complete")
|
||||
setStore("authorization", x.data!)
|
||||
})
|
||||
.catch((e) => {
|
||||
setStore("state", "error")
|
||||
setStore("error", String(e))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let listRef: ListRef | undefined
|
||||
function handleKey(e: KeyboardEvent) {
|
||||
if (e.key === "Enter" && e.target instanceof HTMLInputElement) {
|
||||
return
|
||||
}
|
||||
if (e.key === "Escape") return
|
||||
listRef?.onKeyDown(e)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (methods().length === 1) {
|
||||
selectMethod(0)
|
||||
}
|
||||
document.addEventListener("keydown", handleKey)
|
||||
onCleanup(() => {
|
||||
document.removeEventListener("keydown", handleKey)
|
||||
})
|
||||
})
|
||||
|
||||
async function complete() {
|
||||
await globalSDK.client.global.dispose()
|
||||
dialog.close()
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "circle-check",
|
||||
title: `${provider().name} connected`,
|
||||
description: `${provider().name} models are now available to use.`,
|
||||
})
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
if (methods().length === 1) {
|
||||
dialog.show(() => <DialogSelectProvider />)
|
||||
return
|
||||
}
|
||||
if (store.authorization) {
|
||||
setStore("authorization", undefined)
|
||||
setStore("methodIndex", undefined)
|
||||
return
|
||||
}
|
||||
if (store.methodIndex) {
|
||||
setStore("methodIndex", undefined)
|
||||
return
|
||||
}
|
||||
dialog.show(() => <DialogSelectProvider />)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog title={<IconButton tabIndex={-1} icon="arrow-left" variant="ghost" onClick={goBack} />}>
|
||||
<div class="flex flex-col gap-6 px-2.5 pb-3">
|
||||
<div class="px-2.5 flex gap-4 items-center">
|
||||
<ProviderIcon id={props.provider as IconName} class="size-5 shrink-0 icon-strong-base" />
|
||||
<div class="text-16-medium text-text-strong">
|
||||
<Switch>
|
||||
<Match when={props.provider === "anthropic" && method()?.label?.toLowerCase().includes("max")}>
|
||||
Login with Claude Pro/Max
|
||||
</Match>
|
||||
<Match when={true}>Connect {provider().name}</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-2.5 pb-10 flex flex-col gap-6">
|
||||
<Switch>
|
||||
<Match when={store.methodIndex === undefined}>
|
||||
<div class="text-14-regular text-text-base">Select login method for {provider().name}.</div>
|
||||
<div class="">
|
||||
<List
|
||||
ref={(ref) => (listRef = ref)}
|
||||
items={methods}
|
||||
key={(m) => m?.label}
|
||||
onSelect={async (method, index) => {
|
||||
if (!method) return
|
||||
selectMethod(index)
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center gap-x-4">
|
||||
<div class="w-4 h-2 rounded-[1px] bg-input-base shadow-xs-border-base flex items-center justify-center">
|
||||
<div class="w-2.5 h-0.5 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
|
||||
</div>
|
||||
<span>{i.label}</span>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={store.state === "pending"}>
|
||||
<div class="text-14-regular text-text-base">
|
||||
<div class="flex items-center gap-x-4">
|
||||
<Spinner />
|
||||
<span>Authorization in progress...</span>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={store.state === "error"}>
|
||||
<div class="text-14-regular text-text-base">
|
||||
<div class="flex items-center gap-x-4">
|
||||
<Icon name="circle-ban-sign" class="text-icon-critical-base" />
|
||||
<span>Authorization failed: {store.error}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={method()?.type === "api"}>
|
||||
{iife(() => {
|
||||
const [formStore, setFormStore] = createStore({
|
||||
value: "",
|
||||
error: undefined as string | undefined,
|
||||
})
|
||||
|
||||
async function handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
|
||||
const form = e.currentTarget as HTMLFormElement
|
||||
const formData = new FormData(form)
|
||||
const apiKey = formData.get("apiKey") as string
|
||||
|
||||
if (!apiKey?.trim()) {
|
||||
setFormStore("error", "API key is required")
|
||||
return
|
||||
}
|
||||
|
||||
setFormStore("error", undefined)
|
||||
await globalSDK.client.auth.set({
|
||||
providerID: props.provider,
|
||||
auth: {
|
||||
type: "api",
|
||||
key: apiKey,
|
||||
},
|
||||
})
|
||||
await complete()
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-6">
|
||||
<Switch>
|
||||
<Match when={provider().id === "opencode"}>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="text-14-regular text-text-base">
|
||||
OpenCode Zen gives you access to a curated set of reliable optimized models for coding
|
||||
agents.
|
||||
</div>
|
||||
<div class="text-14-regular text-text-base">
|
||||
With a single API key you'll get access to models such as Claude, GPT, Gemini, GLM and more.
|
||||
</div>
|
||||
<div class="text-14-regular text-text-base">
|
||||
Visit{" "}
|
||||
<Link href="https://opencode.ai/zen" tabIndex={-1}>
|
||||
opencode.ai/zen
|
||||
</Link>{" "}
|
||||
to collect your API key.
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<div class="text-14-regular text-text-base">
|
||||
Enter your {provider().name} API key to connect your account and use {provider().name} models
|
||||
in OpenCode.
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
|
||||
<TextField
|
||||
autofocus
|
||||
type="text"
|
||||
label={`${provider().name} API key`}
|
||||
placeholder="API key"
|
||||
name="apiKey"
|
||||
value={formStore.value}
|
||||
onChange={setFormStore.bind(null, "value")}
|
||||
validationState={formStore.error ? "invalid" : undefined}
|
||||
error={formStore.error}
|
||||
/>
|
||||
<Button class="w-auto" type="submit" size="large" variant="primary">
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Match>
|
||||
<Match when={method()?.type === "oauth"}>
|
||||
<Switch>
|
||||
<Match when={store.authorization?.method === "code"}>
|
||||
{iife(() => {
|
||||
const [formStore, setFormStore] = createStore({
|
||||
value: "",
|
||||
error: undefined as string | undefined,
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
if (store.authorization?.method === "code" && store.authorization?.url) {
|
||||
platform.openLink(store.authorization.url)
|
||||
}
|
||||
})
|
||||
|
||||
async function handleSubmit(e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
|
||||
const form = e.currentTarget as HTMLFormElement
|
||||
const formData = new FormData(form)
|
||||
const code = formData.get("code") as string
|
||||
|
||||
if (!code?.trim()) {
|
||||
setFormStore("error", "Authorization code is required")
|
||||
return
|
||||
}
|
||||
|
||||
setFormStore("error", undefined)
|
||||
const { error } = await globalSDK.client.provider.oauth.callback({
|
||||
providerID: props.provider,
|
||||
method: store.methodIndex,
|
||||
code,
|
||||
})
|
||||
if (!error) {
|
||||
await complete()
|
||||
return
|
||||
}
|
||||
setFormStore("error", "Invalid authorization code")
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="text-14-regular text-text-base">
|
||||
Visit <Link href={store.authorization!.url}>this link</Link> to collect your authorization
|
||||
code to connect your account and use {provider().name} models in OpenCode.
|
||||
</div>
|
||||
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
|
||||
<TextField
|
||||
autofocus
|
||||
type="text"
|
||||
label={`${method()?.label} authorization code`}
|
||||
placeholder="Authorization code"
|
||||
name="code"
|
||||
value={formStore.value}
|
||||
onChange={setFormStore.bind(null, "value")}
|
||||
validationState={formStore.error ? "invalid" : undefined}
|
||||
error={formStore.error}
|
||||
/>
|
||||
<Button class="w-auto" type="submit" size="large" variant="primary">
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Match>
|
||||
<Match when={store.authorization?.method === "auto"}>
|
||||
{iife(() => {
|
||||
const code = createMemo(() => {
|
||||
const instructions = store.authorization?.instructions
|
||||
if (instructions?.includes(":")) {
|
||||
return instructions?.split(":")[1]?.trim()
|
||||
}
|
||||
return instructions
|
||||
})
|
||||
|
||||
onMount(async () => {
|
||||
const result = await globalSDK.client.provider.oauth.callback({
|
||||
providerID: props.provider,
|
||||
method: store.methodIndex,
|
||||
})
|
||||
if (result.error) {
|
||||
// TODO: show error
|
||||
dialog.close()
|
||||
return
|
||||
}
|
||||
await complete()
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="text-14-regular text-text-base">
|
||||
Visit <Link href={store.authorization!.url}>this link</Link> and enter the code below to
|
||||
connect your account and use {provider().name} models in OpenCode.
|
||||
</div>
|
||||
<TextField label="Confirmation code" class="font-mono" value={code()} readOnly copyable />
|
||||
<div class="text-14-regular text-text-base flex items-center gap-4">
|
||||
<Spinner />
|
||||
<span>Waiting for authorization...</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Match>
|
||||
</Switch>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
50
packages/desktop/src/components/dialog-manage-models.tsx
Normal file
50
packages/desktop/src/components/dialog-manage-models.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Component } from "solid-js"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { popularProviders } from "@/hooks/use-providers"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { Switch } from "@opencode-ai/ui/switch"
|
||||
|
||||
export const DialogManageModels: Component = () => {
|
||||
const local = useLocal()
|
||||
return (
|
||||
<Dialog title="Manage models" description="Customize which models appear in the model selector.">
|
||||
<List
|
||||
class="px-2.5"
|
||||
search={{ placeholder: "Search models", autofocus: true }}
|
||||
emptyMessage="No model results"
|
||||
key={(x) => `${x?.provider?.id}:${x?.id}`}
|
||||
items={local.model.list()}
|
||||
filterKeys={["provider.name", "name", "id"]}
|
||||
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
||||
groupBy={(x) => x.provider.name}
|
||||
sortGroupsBy={(a, b) => {
|
||||
const aProvider = a.items[0].provider.id
|
||||
const bProvider = b.items[0].provider.id
|
||||
if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
|
||||
if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
|
||||
return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
|
||||
}}
|
||||
onSelect={(x) => {
|
||||
if (!x) return
|
||||
const visible = local.model.visible({ modelID: x.id, providerID: x.provider.id })
|
||||
local.model.setVisibility({ modelID: x.id, providerID: x.provider.id }, !visible)
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center justify-between gap-x-2.5">
|
||||
<span>{i.name}</span>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<Switch
|
||||
checked={!!local.model.visible({ modelID: i.id, providerID: i.provider.id })}
|
||||
onChange={(checked) => {
|
||||
local.model.setVisibility({ modelID: i.id, providerID: i.provider.id }, checked)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
49
packages/desktop/src/components/dialog-select-file.tsx
Normal file
49
packages/desktop/src/components/dialog-select-file.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useLocal } from "@/context/local"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { createMemo } from "solid-js"
|
||||
|
||||
export function DialogSelectFile() {
|
||||
const layout = useLayout()
|
||||
const local = useLocal()
|
||||
const dialog = useDialog()
|
||||
const params = useParams()
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
||||
return (
|
||||
<Dialog title="Select file">
|
||||
<List
|
||||
class="px-2.5"
|
||||
search={{ placeholder: "Search files", autofocus: true }}
|
||||
emptyMessage="No files found"
|
||||
items={local.file.searchFiles}
|
||||
key={(x) => x}
|
||||
onSelect={(path) => {
|
||||
if (path) {
|
||||
tabs().open("file://" + path)
|
||||
}
|
||||
dialog.close()
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center justify-between rounded-md">
|
||||
<div class="flex items-center gap-x-2 grow min-w-0">
|
||||
<FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
|
||||
<div class="flex items-center text-14-regular">
|
||||
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
|
||||
{getDirectory(i)}
|
||||
</span>
|
||||
<span class="text-text-strong whitespace-nowrap">{getFilename(i)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
119
packages/desktop/src/components/dialog-select-model-unpaid.tsx
Normal file
119
packages/desktop/src/components/dialog-select-model-unpaid.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { Component, onCleanup, onMount, Show } from "solid-js"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Tag } from "@opencode-ai/ui/tag"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List, ListRef } from "@opencode-ai/ui/list"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||
import { DialogConnectProvider } from "./dialog-connect-provider"
|
||||
|
||||
export const DialogSelectModelUnpaid: Component = () => {
|
||||
const local = useLocal()
|
||||
const dialog = useDialog()
|
||||
const providers = useProviders()
|
||||
|
||||
let listRef: ListRef | undefined
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") return
|
||||
listRef?.onKeyDown(e)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener("keydown", handleKey)
|
||||
onCleanup(() => {
|
||||
document.removeEventListener("keydown", handleKey)
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog title="Select model">
|
||||
<div class="flex flex-col gap-3 px-2.5">
|
||||
<div class="text-14-medium text-text-base px-2.5">Free models provided by OpenCode</div>
|
||||
<List
|
||||
ref={(ref) => (listRef = ref)}
|
||||
items={local.model.list}
|
||||
current={local.model.current()}
|
||||
key={(x) => `${x.provider.id}:${x.id}`}
|
||||
onSelect={(x) => {
|
||||
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
|
||||
recent: true,
|
||||
})
|
||||
dialog.close()
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center gap-x-2.5">
|
||||
<span>{i.name}</span>
|
||||
<Tag>Free</Tag>
|
||||
<Show when={i.latest}>
|
||||
<Tag>Latest</Tag>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div class="px-1.5 pb-1.5">
|
||||
<div class="w-full rounded-sm border border-border-weak-base bg-surface-raised-base">
|
||||
<div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-4">
|
||||
<div class="px-2 text-14-medium text-text-base">Add more models from popular providers</div>
|
||||
<div class="w-full">
|
||||
<List
|
||||
class="w-full"
|
||||
key={(x) => x?.id}
|
||||
items={providers.popular}
|
||||
activeIcon="plus-small"
|
||||
sortBy={(a, b) => {
|
||||
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
|
||||
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
|
||||
return a.name.localeCompare(b.name)
|
||||
}}
|
||||
onSelect={(x) => {
|
||||
if (!x) return
|
||||
dialog.show(() => <DialogConnectProvider provider={x.id} />)
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center gap-x-4">
|
||||
<ProviderIcon
|
||||
data-slot="list-item-extra-icon"
|
||||
id={i.id as IconName}
|
||||
// TODO: clean this up after we update icon in models.dev
|
||||
classList={{
|
||||
"text-icon-weak-base": true,
|
||||
"size-4 mx-0.5": i.id === "opencode",
|
||||
"size-5": i.id !== "opencode",
|
||||
}}
|
||||
/>
|
||||
<span>{i.name}</span>
|
||||
<Show when={i.id === "opencode"}>
|
||||
<Tag>Recommended</Tag>
|
||||
</Show>
|
||||
<Show when={i.id === "anthropic"}>
|
||||
<div class="text-14-regular text-text-weak">Connect with Claude Pro/Max or API key</div>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="w-full justify-start px-[11px] py-3.5 gap-4.5 text-14-medium"
|
||||
icon="dot-grid"
|
||||
onClick={() => {
|
||||
dialog.show(() => <DialogSelectProvider />)
|
||||
}}
|
||||
>
|
||||
View all providers
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
84
packages/desktop/src/components/dialog-select-model.tsx
Normal file
84
packages/desktop/src/components/dialog-select-model.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { Component, createMemo, Show } from "solid-js"
|
||||
import { useLocal } from "@/context/local"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { popularProviders } from "@/hooks/use-providers"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Tag } from "@opencode-ai/ui/tag"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { DialogSelectProvider } from "./dialog-select-provider"
|
||||
import { DialogManageModels } from "./dialog-manage-models"
|
||||
|
||||
export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
|
||||
const local = useLocal()
|
||||
const dialog = useDialog()
|
||||
|
||||
const models = createMemo(() =>
|
||||
local.model
|
||||
.list()
|
||||
.filter((m) => local.model.visible({ modelID: m.id, providerID: m.provider.id }))
|
||||
.filter((m) => (props.provider ? m.provider.id === props.provider : true)),
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title="Select model"
|
||||
action={
|
||||
<Button
|
||||
class="h-7 -my-1 text-14-medium"
|
||||
icon="plus-small"
|
||||
tabIndex={-1}
|
||||
onClick={() => dialog.show(() => <DialogSelectProvider />)}
|
||||
>
|
||||
Connect provider
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<List
|
||||
class="px-2.5"
|
||||
search={{ placeholder: "Search models", autofocus: true }}
|
||||
emptyMessage="No model results"
|
||||
key={(x) => `${x.provider.id}:${x.id}`}
|
||||
items={models}
|
||||
current={local.model.current()}
|
||||
filterKeys={["provider.name", "name", "id"]}
|
||||
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
||||
groupBy={(x) => x.provider.name}
|
||||
sortGroupsBy={(a, b) => {
|
||||
if (a.category === "Recent" && b.category !== "Recent") return -1
|
||||
if (b.category === "Recent" && a.category !== "Recent") return 1
|
||||
const aProvider = a.items[0].provider.id
|
||||
const bProvider = b.items[0].provider.id
|
||||
if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
|
||||
if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
|
||||
return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
|
||||
}}
|
||||
onSelect={(x) => {
|
||||
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
|
||||
recent: true,
|
||||
})
|
||||
dialog.close()
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="w-full flex items-center gap-x-2.5">
|
||||
<span>{i.name}</span>
|
||||
<Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}>
|
||||
<Tag>Free</Tag>
|
||||
</Show>
|
||||
<Show when={i.latest}>
|
||||
<Tag>Latest</Tag>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="ml-3 mt-5 mb-6 text-text-base self-start"
|
||||
onClick={() => dialog.show(() => <DialogManageModels />)}
|
||||
>
|
||||
Manage models
|
||||
</Button>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
64
packages/desktop/src/components/dialog-select-provider.tsx
Normal file
64
packages/desktop/src/components/dialog-select-provider.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Component, Show } from "solid-js"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { popularProviders, useProviders } from "@/hooks/use-providers"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
import { Tag } from "@opencode-ai/ui/tag"
|
||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||
import { IconName } from "@opencode-ai/ui/icons/provider"
|
||||
import { DialogConnectProvider } from "./dialog-connect-provider"
|
||||
|
||||
export const DialogSelectProvider: Component = () => {
|
||||
const dialog = useDialog()
|
||||
const providers = useProviders()
|
||||
|
||||
return (
|
||||
<Dialog title="Connect provider">
|
||||
<List
|
||||
class="px-2.5"
|
||||
search={{ placeholder: "Search providers", autofocus: true }}
|
||||
activeIcon="plus-small"
|
||||
key={(x) => x?.id}
|
||||
items={providers.all}
|
||||
filterKeys={["id", "name"]}
|
||||
groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")}
|
||||
sortBy={(a, b) => {
|
||||
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
|
||||
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
|
||||
return a.name.localeCompare(b.name)
|
||||
}}
|
||||
sortGroupsBy={(a, b) => {
|
||||
if (a.category === "Popular" && b.category !== "Popular") return -1
|
||||
if (b.category === "Popular" && a.category !== "Popular") return 1
|
||||
return 0
|
||||
}}
|
||||
onSelect={(x) => {
|
||||
if (!x) return
|
||||
dialog.show(() => <DialogConnectProvider provider={x.id} />)
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div class="px-1.25 w-full flex items-center gap-x-4">
|
||||
<ProviderIcon
|
||||
data-slot="list-item-extra-icon"
|
||||
id={i.id as IconName}
|
||||
// TODO: clean this up after we update icon in models.dev
|
||||
classList={{
|
||||
"text-icon-weak-base": true,
|
||||
"size-4 mx-0.5": i.id === "opencode",
|
||||
"size-5": i.id !== "opencode",
|
||||
}}
|
||||
/>
|
||||
<span>{i.name}</span>
|
||||
<Show when={i.id === "opencode"}>
|
||||
<Tag>Recommended</Tag>
|
||||
</Show>
|
||||
<Show when={i.id === "anthropic"}>
|
||||
<div class="text-14-regular text-text-weak">Connect with Claude Pro/Max or API key</div>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
113
packages/desktop/src/components/header.tsx
Normal file
113
packages/desktop/src/components/header.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { Session } from "@opencode-ai/sdk/v2/client"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { Mark } from "@opencode-ai/ui/logo"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { base64Decode } from "@opencode-ai/util/encode"
|
||||
import { getFilename } from "@opencode-ai/util/path"
|
||||
import { A, useParams } from "@solidjs/router"
|
||||
import { createMemo, Show } from "solid-js"
|
||||
|
||||
export function Header(props: {
|
||||
navigateToProject: (directory: string) => void
|
||||
navigateToSession: (session: Session | undefined) => void
|
||||
}) {
|
||||
const globalSync = useGlobalSync()
|
||||
const layout = useLayout()
|
||||
const params = useParams()
|
||||
const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
|
||||
const store = createMemo(() => globalSync.child(currentDirectory())[0])
|
||||
const sessions = createMemo(() => store().session ?? [])
|
||||
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
|
||||
|
||||
return (
|
||||
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
|
||||
<A
|
||||
href="/"
|
||||
classList={{
|
||||
"w-12 shrink-0 px-4 py-3.5": true,
|
||||
"flex items-center justify-start self-stretch": true,
|
||||
"border-r border-border-weak-base": true,
|
||||
}}
|
||||
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<Mark class="shrink-0" />
|
||||
</A>
|
||||
<div class="pl-4 px-6 flex items-center justify-between gap-4 w-full">
|
||||
<Show when={params.dir && layout.projects.list().length > 0}>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Select
|
||||
options={layout.projects.list().map((project) => project.worktree)}
|
||||
current={currentDirectory()}
|
||||
label={(x) => getFilename(x)}
|
||||
onSelect={(x) => (x ? props.navigateToProject(x) : undefined)}
|
||||
class="text-14-regular text-text-base"
|
||||
variant="ghost"
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
{(i) => (
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon name="folder" size="small" />
|
||||
<div class="text-text-strong">{getFilename(i)}</div>
|
||||
</div>
|
||||
)}
|
||||
</Select>
|
||||
<div class="text-text-weaker">/</div>
|
||||
<Select
|
||||
options={sessions()}
|
||||
current={currentSession()}
|
||||
placeholder="New session"
|
||||
label={(x) => x.title}
|
||||
value={(x) => x.id}
|
||||
onSelect={props.navigateToSession}
|
||||
class="text-14-regular text-text-base max-w-md"
|
||||
variant="ghost"
|
||||
/>
|
||||
</div>
|
||||
<Show when={currentSession()}>
|
||||
<Button as={A} href={`/${params.dir}/session`} icon="plus-small">
|
||||
New session
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<Tooltip
|
||||
class="shrink-0"
|
||||
value={
|
||||
<div class="flex items-center gap-2">
|
||||
<span>Toggle terminal</span>
|
||||
<span class="text-icon-base text-12-medium">Ctrl `</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
|
||||
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||
<Icon
|
||||
size="small"
|
||||
name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
|
||||
class="group-hover/terminal-toggle:hidden"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name="layout-bottom-partial"
|
||||
class="hidden group-hover/terminal-toggle:inline-block"
|
||||
/>
|
||||
<Icon
|
||||
size="small"
|
||||
name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
|
||||
class="hidden group-active/terminal-toggle:inline-block"
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,9 @@
|
||||
import { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
|
||||
import { ComponentProps, onCleanup, onMount, splitProps } from "solid-js"
|
||||
import { ComponentProps, createEffect, onCleanup, onMount, splitProps } from "solid-js"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { SerializeAddon } from "@/addons/serialize"
|
||||
import { LocalPTY } from "@/context/session"
|
||||
import { LocalPTY } from "@/context/terminal"
|
||||
import { usePrefersDark } from "@solid-primitives/media"
|
||||
|
||||
export interface TerminalProps extends ComponentProps<"div"> {
|
||||
pty: LocalPTY
|
||||
@@ -21,6 +22,7 @@ export const Terminal = (props: TerminalProps) => {
|
||||
let serializeAddon: SerializeAddon
|
||||
let fitAddon: FitAddon
|
||||
let handleResize: () => void
|
||||
const prefersDark = usePrefersDark()
|
||||
|
||||
onMount(async () => {
|
||||
ghostty = await Ghostty.load()
|
||||
@@ -31,10 +33,17 @@ export const Terminal = (props: TerminalProps) => {
|
||||
fontSize: 14,
|
||||
fontFamily: "TX-02, monospace",
|
||||
allowTransparency: true,
|
||||
theme: {
|
||||
background: "#191515",
|
||||
foreground: "#d4d4d4",
|
||||
},
|
||||
theme: prefersDark()
|
||||
? {
|
||||
background: "#191515",
|
||||
foreground: "#d4d4d4",
|
||||
cursor: "#d4d4d4",
|
||||
}
|
||||
: {
|
||||
background: "#fcfcfc",
|
||||
foreground: "#211e1e",
|
||||
cursor: "#211e1e",
|
||||
},
|
||||
scrollback: 10_000,
|
||||
ghostty,
|
||||
})
|
||||
|
||||
239
packages/desktop/src/context/command.tsx
Normal file
239
packages/desktop/src/context/command.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
import { createMemo, createSignal, onCleanup, onMount, Show, type Accessor } from "solid-js"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { Dialog } from "@opencode-ai/ui/dialog"
|
||||
import { List } from "@opencode-ai/ui/list"
|
||||
|
||||
const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)
|
||||
|
||||
export type KeybindConfig = string
|
||||
|
||||
export interface Keybind {
|
||||
key: string
|
||||
ctrl: boolean
|
||||
meta: boolean
|
||||
shift: boolean
|
||||
alt: boolean
|
||||
}
|
||||
|
||||
export interface CommandOption {
|
||||
id: string
|
||||
title: string
|
||||
description?: string
|
||||
category?: string
|
||||
keybind?: KeybindConfig
|
||||
slash?: string
|
||||
suggested?: boolean
|
||||
disabled?: boolean
|
||||
onSelect?: (source?: "palette" | "keybind" | "slash") => void
|
||||
}
|
||||
|
||||
export function parseKeybind(config: string): Keybind[] {
|
||||
if (!config || config === "none") return []
|
||||
|
||||
return config.split(",").map((combo) => {
|
||||
const parts = combo.trim().toLowerCase().split("+")
|
||||
const keybind: Keybind = {
|
||||
key: "",
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
alt: false,
|
||||
}
|
||||
|
||||
for (const part of parts) {
|
||||
switch (part) {
|
||||
case "ctrl":
|
||||
case "control":
|
||||
keybind.ctrl = true
|
||||
break
|
||||
case "meta":
|
||||
case "cmd":
|
||||
case "command":
|
||||
keybind.meta = true
|
||||
break
|
||||
case "mod":
|
||||
if (IS_MAC) keybind.meta = true
|
||||
else keybind.ctrl = true
|
||||
break
|
||||
case "alt":
|
||||
case "option":
|
||||
keybind.alt = true
|
||||
break
|
||||
case "shift":
|
||||
keybind.shift = true
|
||||
break
|
||||
default:
|
||||
keybind.key = part
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return keybind
|
||||
})
|
||||
}
|
||||
|
||||
export function matchKeybind(keybinds: Keybind[], event: KeyboardEvent): boolean {
|
||||
const eventKey = event.key.toLowerCase()
|
||||
|
||||
for (const kb of keybinds) {
|
||||
const keyMatch = kb.key === eventKey
|
||||
const ctrlMatch = kb.ctrl === (event.ctrlKey || false)
|
||||
const metaMatch = kb.meta === (event.metaKey || false)
|
||||
const shiftMatch = kb.shift === (event.shiftKey || false)
|
||||
const altMatch = kb.alt === (event.altKey || false)
|
||||
|
||||
if (keyMatch && ctrlMatch && metaMatch && shiftMatch && altMatch) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function formatKeybind(config: string): string {
|
||||
if (!config || config === "none") return ""
|
||||
|
||||
const keybinds = parseKeybind(config)
|
||||
if (keybinds.length === 0) return ""
|
||||
|
||||
const kb = keybinds[0]
|
||||
const parts: string[] = []
|
||||
|
||||
if (kb.ctrl) parts.push(IS_MAC ? "⌃" : "Ctrl")
|
||||
if (kb.alt) parts.push(IS_MAC ? "⌥" : "Alt")
|
||||
if (kb.shift) parts.push(IS_MAC ? "⇧" : "Shift")
|
||||
if (kb.meta) parts.push(IS_MAC ? "⌘" : "Meta")
|
||||
|
||||
if (kb.key) {
|
||||
const displayKey = kb.key.length === 1 ? kb.key.toUpperCase() : kb.key.charAt(0).toUpperCase() + kb.key.slice(1)
|
||||
parts.push(displayKey)
|
||||
}
|
||||
|
||||
return IS_MAC ? parts.join("") : parts.join("+")
|
||||
}
|
||||
|
||||
function DialogCommand(props: { options: CommandOption[] }) {
|
||||
const dialog = useDialog()
|
||||
|
||||
return (
|
||||
<Dialog title="Commands">
|
||||
<List
|
||||
class="px-2.5"
|
||||
search={{ placeholder: "Search commands", autofocus: true }}
|
||||
emptyMessage="No commands found"
|
||||
items={() => props.options.filter((x) => !x.id.startsWith("suggested.") || !x.disabled)}
|
||||
key={(x) => x?.id}
|
||||
filterKeys={["title", "description", "category"]}
|
||||
groupBy={(x) => x.category ?? ""}
|
||||
onSelect={(option) => {
|
||||
if (option) {
|
||||
dialog.close()
|
||||
option.onSelect?.("palette")
|
||||
}
|
||||
}}
|
||||
>
|
||||
{(option) => (
|
||||
<div class="w-full flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span class="text-14-regular text-text-strong whitespace-nowrap">{option.title}</span>
|
||||
<Show when={option.description}>
|
||||
<span class="text-14-regular text-text-weak truncate">{option.description}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={option.keybind}>
|
||||
<span class="text-12-regular text-text-subtle shrink-0">{formatKeybind(option.keybind!)}</span>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</List>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export const { use: useCommand, provider: CommandProvider } = createSimpleContext({
|
||||
name: "Command",
|
||||
init: () => {
|
||||
const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([])
|
||||
const [suspendCount, setSuspendCount] = createSignal(0)
|
||||
const dialog = useDialog()
|
||||
|
||||
const options = createMemo(() => {
|
||||
const all = registrations().flatMap((x) => x())
|
||||
const suggested = all.filter((x) => x.suggested && !x.disabled)
|
||||
return [
|
||||
...suggested.map((x) => ({
|
||||
...x,
|
||||
id: "suggested." + x.id,
|
||||
category: "Suggested",
|
||||
})),
|
||||
...all,
|
||||
]
|
||||
})
|
||||
|
||||
const suspended = () => suspendCount() > 0
|
||||
|
||||
const showPalette = () => {
|
||||
if (!dialog.active) {
|
||||
dialog.show(() => <DialogCommand options={options().filter((x) => !x.disabled)} />)
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (suspended()) return
|
||||
|
||||
const paletteKeybinds = parseKeybind("mod+shift+p")
|
||||
if (matchKeybind(paletteKeybinds, event)) {
|
||||
event.preventDefault()
|
||||
showPalette()
|
||||
return
|
||||
}
|
||||
|
||||
for (const option of options()) {
|
||||
if (option.disabled) continue
|
||||
if (!option.keybind) continue
|
||||
|
||||
const keybinds = parseKeybind(option.keybind)
|
||||
if (matchKeybind(keybinds, event)) {
|
||||
event.preventDefault()
|
||||
option.onSelect?.("keybind")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener("keydown", handleKeyDown)
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
document.removeEventListener("keydown", handleKeyDown)
|
||||
})
|
||||
|
||||
return {
|
||||
register(cb: () => CommandOption[]) {
|
||||
const results = createMemo(cb)
|
||||
setRegistrations((arr) => [results, ...arr])
|
||||
onCleanup(() => {
|
||||
setRegistrations((arr) => arr.filter((x) => x !== results))
|
||||
})
|
||||
},
|
||||
trigger(id: string, source?: "palette" | "keybind" | "slash") {
|
||||
for (const option of options()) {
|
||||
if (option.id === id || option.id === "suggested." + id) {
|
||||
option.onSelect?.(source)
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
show: showPalette,
|
||||
keybinds(enabled: boolean) {
|
||||
setSuspendCount((count) => count + (enabled ? -1 : 1))
|
||||
},
|
||||
suspended,
|
||||
get options() {
|
||||
return options()
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type SessionStatus,
|
||||
type ProviderListResponse,
|
||||
type ProviderAuthResponse,
|
||||
type Command,
|
||||
createOpencodeClient,
|
||||
} from "@opencode-ai/sdk/v2/client"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
@@ -24,6 +25,7 @@ import { onMount } from "solid-js"
|
||||
type State = {
|
||||
ready: boolean
|
||||
agent: Agent[]
|
||||
command: Command[]
|
||||
project: string
|
||||
provider: ProviderListResponse
|
||||
config: Config
|
||||
@@ -55,45 +57,20 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
|
||||
const globalSDK = useGlobalSDK()
|
||||
const [globalStore, setGlobalStore] = createStore<{
|
||||
ready: boolean
|
||||
path: Path
|
||||
project: Project[]
|
||||
provider: ProviderListResponse
|
||||
provider_auth: ProviderAuthResponse
|
||||
children: Record<string, State>
|
||||
}>({
|
||||
ready: false,
|
||||
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||
project: [],
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
provider_auth: {},
|
||||
children: {},
|
||||
})
|
||||
|
||||
async function bootstrapInstance(directory: string) {
|
||||
const [store, setStore] = child(directory)
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: globalSDK.url,
|
||||
directory,
|
||||
})
|
||||
const load = {
|
||||
project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
|
||||
provider: () => sdk.provider.list().then((x) => setStore("provider", x.data!)),
|
||||
path: () => sdk.path.get().then((x) => setStore("path", x.data!)),
|
||||
agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
|
||||
session: () =>
|
||||
sdk.session.list().then((x) => {
|
||||
const sessions = (x.data ?? [])
|
||||
.slice()
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
.slice(0, store.limit)
|
||||
setStore("session", sessions)
|
||||
}),
|
||||
status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)),
|
||||
config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
|
||||
changes: () => sdk.file.status().then((x) => setStore("changes", x.data!)),
|
||||
node: () => sdk.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
|
||||
}
|
||||
await Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
|
||||
}
|
||||
|
||||
const children: Record<string, ReturnType<typeof createStore<State>>> = {}
|
||||
function child(directory: string) {
|
||||
if (!children[directory]) {
|
||||
@@ -104,6 +81,7 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
|
||||
path: { state: "", config: "", worktree: "", directory: "", home: "" },
|
||||
ready: false,
|
||||
agent: [],
|
||||
command: [],
|
||||
session: [],
|
||||
session_status: {},
|
||||
session_diff: {},
|
||||
@@ -120,12 +98,51 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
|
||||
return children[directory]
|
||||
}
|
||||
|
||||
async function loadSessions(directory: string) {
|
||||
globalSDK.client.session.list({ directory }).then((x) => {
|
||||
const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000
|
||||
const nonArchived = (x.data ?? [])
|
||||
.slice()
|
||||
.filter((s) => !s.time.archived)
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
// Include at least 5 sessions, plus any updated in the last hour
|
||||
const sessions = nonArchived.filter((s, i) => {
|
||||
if (i < 5) return true
|
||||
const updated = new Date(s.time.updated).getTime()
|
||||
return updated > fourHoursAgo
|
||||
})
|
||||
const [, setStore] = child(directory)
|
||||
setStore("session", sessions)
|
||||
})
|
||||
}
|
||||
|
||||
async function bootstrapInstance(directory: string) {
|
||||
const [, setStore] = child(directory)
|
||||
const sdk = createOpencodeClient({
|
||||
baseUrl: globalSDK.url,
|
||||
directory,
|
||||
})
|
||||
const load = {
|
||||
project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
|
||||
provider: () => sdk.provider.list().then((x) => setStore("provider", x.data!)),
|
||||
path: () => sdk.path.get().then((x) => setStore("path", x.data!)),
|
||||
agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
|
||||
command: () => sdk.command.list().then((x) => setStore("command", x.data ?? [])),
|
||||
session: () => loadSessions(directory),
|
||||
status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)),
|
||||
config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
|
||||
changes: () => sdk.file.status().then((x) => setStore("changes", x.data!)),
|
||||
node: () => sdk.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
|
||||
}
|
||||
await Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
|
||||
}
|
||||
|
||||
globalSDK.event.listen((e) => {
|
||||
const directory = e.name
|
||||
const event = e.details
|
||||
|
||||
if (directory === "global") {
|
||||
switch (event.type) {
|
||||
switch (event?.type) {
|
||||
case "global.disposed": {
|
||||
bootstrap()
|
||||
break
|
||||
@@ -156,6 +173,17 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
|
||||
}
|
||||
case "session.updated": {
|
||||
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
|
||||
if (event.properties.info.time.archived) {
|
||||
if (result.found) {
|
||||
setStore(
|
||||
"session",
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 1)
|
||||
}),
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
if (result.found) {
|
||||
setStore("session", result.index, reconcile(event.properties.info))
|
||||
break
|
||||
@@ -198,6 +226,21 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
|
||||
)
|
||||
break
|
||||
}
|
||||
case "message.removed": {
|
||||
const messages = store.message[event.properties.sessionID]
|
||||
if (!messages) break
|
||||
const result = Binary.search(messages, event.properties.messageID, (m) => m.id)
|
||||
if (result.found) {
|
||||
setStore(
|
||||
"message",
|
||||
event.properties.sessionID,
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 1)
|
||||
}),
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "message.part.updated": {
|
||||
const part = event.properties.part
|
||||
const parts = store.part[part.messageID]
|
||||
@@ -219,17 +262,33 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
|
||||
)
|
||||
break
|
||||
}
|
||||
case "message.part.removed": {
|
||||
const parts = store.part[event.properties.messageID]
|
||||
if (!parts) break
|
||||
const result = Binary.search(parts, event.properties.partID, (p) => p.id)
|
||||
if (result.found) {
|
||||
setStore(
|
||||
"part",
|
||||
event.properties.messageID,
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 1)
|
||||
}),
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function bootstrap() {
|
||||
return Promise.all([
|
||||
globalSDK.client.path.get().then((x) => {
|
||||
setGlobalStore("path", x.data!)
|
||||
}),
|
||||
globalSDK.client.project.list().then(async (x) => {
|
||||
setGlobalStore(
|
||||
"project",
|
||||
x
|
||||
.data!.filter((p) => !p.worktree.includes("opencode-test") && p.vcs)
|
||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
||||
x.data!.filter((p) => !p.worktree.includes("opencode-test")).sort((a, b) => a.id.localeCompare(b.id)),
|
||||
)
|
||||
}),
|
||||
globalSDK.client.provider.list().then((x) => {
|
||||
@@ -252,6 +311,9 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
|
||||
},
|
||||
child,
|
||||
bootstrap,
|
||||
project: {
|
||||
loadSessions,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -7,15 +7,10 @@ import { useGlobalSDK } from "./global-sdk"
|
||||
import { Project } from "@opencode-ai/sdk/v2"
|
||||
|
||||
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
|
||||
|
||||
export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number]
|
||||
|
||||
export function isAvatarColorKey(value: string): value is AvatarColorKey {
|
||||
return AVATAR_COLOR_KEYS.includes(value as AvatarColorKey)
|
||||
}
|
||||
|
||||
export function getAvatarColors(key?: string) {
|
||||
if (key && isAvatarColorKey(key)) {
|
||||
if (key && AVATAR_COLOR_KEYS.includes(key as AvatarColorKey)) {
|
||||
return {
|
||||
background: `var(--avatar-background-${key})`,
|
||||
foreground: `var(--avatar-text-${key})`,
|
||||
@@ -27,7 +22,10 @@ export function getAvatarColors(key?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
type Dialog = "provider" | "model" | "connect"
|
||||
type SessionTabs = {
|
||||
active?: string
|
||||
all: string[]
|
||||
}
|
||||
|
||||
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
|
||||
name: "Layout",
|
||||
@@ -48,24 +46,13 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
review: {
|
||||
state: "pane" as "pane" | "tab",
|
||||
},
|
||||
sessionTabs: {} as Record<string, SessionTabs>,
|
||||
}),
|
||||
{
|
||||
name: "default-layout.v7",
|
||||
name: "layout.v3",
|
||||
},
|
||||
)
|
||||
const [ephemeral, setEphemeral] = createStore<{
|
||||
connect: {
|
||||
provider?: string
|
||||
state?: "pending" | "complete" | "error"
|
||||
error?: string
|
||||
}
|
||||
dialog: {
|
||||
open?: Dialog
|
||||
}
|
||||
}>({
|
||||
connect: {},
|
||||
dialog: {},
|
||||
})
|
||||
|
||||
const usedColors = new Set<AvatarColorKey>()
|
||||
|
||||
function pickAvailableColor(): AvatarColorKey {
|
||||
@@ -97,21 +84,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
const enriched = createMemo(() => store.projects.flatMap(enrich))
|
||||
const list = createMemo(() => enriched().flatMap(colorize))
|
||||
|
||||
async function loadProjectSessions(directory: string) {
|
||||
const [, setStore] = globalSync.child(directory)
|
||||
globalSdk.client.session.list({ directory }).then((x) => {
|
||||
const sessions = (x.data ?? [])
|
||||
.slice()
|
||||
.sort((a, b) => a.id.localeCompare(b.id))
|
||||
.slice(0, 5)
|
||||
setStore("session", sessions)
|
||||
})
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
Promise.all(
|
||||
store.projects.map((project) => {
|
||||
return loadProjectSessions(project.worktree)
|
||||
return globalSync.project.loadSessions(project.worktree)
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -121,7 +97,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
list,
|
||||
open(directory: string) {
|
||||
if (store.projects.find((x) => x.worktree === directory)) return
|
||||
loadProjectSessions(directory)
|
||||
globalSync.project.loadSessions(directory)
|
||||
setStore("projects", (x) => [{ worktree: directory, expanded: true }, ...x])
|
||||
},
|
||||
close(directory: string) {
|
||||
@@ -185,57 +161,85 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
||||
setStore("review", "state", "tab")
|
||||
},
|
||||
},
|
||||
dialog: {
|
||||
opened: createMemo(() => ephemeral.dialog?.open),
|
||||
open(dialog: Dialog) {
|
||||
batch(() => {
|
||||
// if (dialog !== "connect") {
|
||||
// setEphemeral("connect", {})
|
||||
// }
|
||||
setEphemeral("dialog", "open", dialog)
|
||||
})
|
||||
},
|
||||
close(dialog: Dialog) {
|
||||
if (ephemeral.dialog.open === dialog) {
|
||||
setEphemeral(
|
||||
produce((state) => {
|
||||
state.dialog.open = undefined
|
||||
state.connect = {}
|
||||
tabs(sessionKey: string) {
|
||||
const tabs = createMemo(() => store.sessionTabs[sessionKey] ?? { all: [] })
|
||||
return {
|
||||
tabs,
|
||||
active: createMemo(() => tabs().active),
|
||||
all: createMemo(() => tabs().all),
|
||||
setActive(tab: string | undefined) {
|
||||
if (!store.sessionTabs[sessionKey]) {
|
||||
setStore("sessionTabs", sessionKey, { all: [], active: tab })
|
||||
} else {
|
||||
setStore("sessionTabs", sessionKey, "active", tab)
|
||||
}
|
||||
},
|
||||
setAll(all: string[]) {
|
||||
if (!store.sessionTabs[sessionKey]) {
|
||||
setStore("sessionTabs", sessionKey, { all, active: undefined })
|
||||
} else {
|
||||
setStore("sessionTabs", sessionKey, "all", all)
|
||||
}
|
||||
},
|
||||
async open(tab: string) {
|
||||
if (tab === "chat") {
|
||||
if (!store.sessionTabs[sessionKey]) {
|
||||
setStore("sessionTabs", sessionKey, { all: [], active: undefined })
|
||||
} else {
|
||||
setStore("sessionTabs", sessionKey, "active", undefined)
|
||||
}
|
||||
return
|
||||
}
|
||||
const current = store.sessionTabs[sessionKey] ?? { all: [] }
|
||||
if (tab !== "review") {
|
||||
if (!current.all.includes(tab)) {
|
||||
if (!store.sessionTabs[sessionKey]) {
|
||||
setStore("sessionTabs", sessionKey, { all: [tab], active: tab })
|
||||
} else {
|
||||
setStore("sessionTabs", sessionKey, "all", [...current.all, tab])
|
||||
setStore("sessionTabs", sessionKey, "active", tab)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
if (!store.sessionTabs[sessionKey]) {
|
||||
setStore("sessionTabs", sessionKey, { all: [], active: tab })
|
||||
} else {
|
||||
setStore("sessionTabs", sessionKey, "active", tab)
|
||||
}
|
||||
},
|
||||
close(tab: string) {
|
||||
const current = store.sessionTabs[sessionKey]
|
||||
if (!current) return
|
||||
batch(() => {
|
||||
setStore(
|
||||
"sessionTabs",
|
||||
sessionKey,
|
||||
"all",
|
||||
current.all.filter((x) => x !== tab),
|
||||
)
|
||||
if (current.active === tab) {
|
||||
const index = current.all.findIndex((f) => f === tab)
|
||||
const previous = current.all[Math.max(0, index - 1)]
|
||||
setStore("sessionTabs", sessionKey, "active", previous)
|
||||
}
|
||||
})
|
||||
},
|
||||
move(tab: string, to: number) {
|
||||
const current = store.sessionTabs[sessionKey]
|
||||
if (!current) return
|
||||
const index = current.all.findIndex((f) => f === tab)
|
||||
if (index === -1) return
|
||||
setStore(
|
||||
"sessionTabs",
|
||||
sessionKey,
|
||||
"all",
|
||||
produce((opened) => {
|
||||
opened.splice(to, 0, opened.splice(index, 1)[0])
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
connect(provider: string) {
|
||||
setEphemeral(
|
||||
produce((state) => {
|
||||
state.dialog.open = "connect"
|
||||
state.connect = { provider, state: "pending" }
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
connect: {
|
||||
provider: createMemo(() => ephemeral.connect.provider),
|
||||
state: createMemo(() => ephemeral.connect.state),
|
||||
complete() {
|
||||
setEphemeral(
|
||||
produce((state) => {
|
||||
state.dialog.open = "model"
|
||||
state.connect.state = "complete"
|
||||
}),
|
||||
)
|
||||
},
|
||||
error(message: string) {
|
||||
setEphemeral(
|
||||
produce((state) => {
|
||||
state.connect.state = "error"
|
||||
state.connect.error = message
|
||||
}),
|
||||
)
|
||||
},
|
||||
clear() {
|
||||
setEphemeral("connect", {})
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { batch, createEffect, createMemo } from "solid-js"
|
||||
import { uniqueBy } from "remeda"
|
||||
import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda"
|
||||
import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { useSDK } from "./sdk"
|
||||
import { useSync } from "./sync"
|
||||
import { base64Encode } from "@opencode-ai/util/encode"
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { makePersisted } from "@solid-primitives/storage"
|
||||
import { DateTime } from "luxon"
|
||||
|
||||
export type LocalFile = FileNode &
|
||||
Partial<{
|
||||
@@ -78,7 +80,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
})
|
||||
|
||||
const agent = (() => {
|
||||
const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent"))
|
||||
const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden))
|
||||
const [store, setStore] = createStore<{
|
||||
current: string
|
||||
}>({
|
||||
@@ -108,30 +110,62 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
})()
|
||||
|
||||
const model = (() => {
|
||||
const [store, setStore] = createStore<{
|
||||
const [store, setStore] = makePersisted(
|
||||
createStore<{
|
||||
user: (ModelKey & { visibility: "show" | "hide"; favorite?: boolean })[]
|
||||
recent: ModelKey[]
|
||||
}>({
|
||||
user: [],
|
||||
recent: [],
|
||||
}),
|
||||
{ name: "model.v1" },
|
||||
)
|
||||
|
||||
const [ephemeral, setEphemeral] = createStore<{
|
||||
model: Record<string, ModelKey>
|
||||
recent: ModelKey[]
|
||||
}>({
|
||||
model: {},
|
||||
recent: [],
|
||||
})
|
||||
|
||||
const value = localStorage.getItem("model")
|
||||
setStore("recent", JSON.parse(value ?? "[]"))
|
||||
createEffect(() => {
|
||||
localStorage.setItem("model", JSON.stringify(store.recent))
|
||||
})
|
||||
|
||||
const list = createMemo(() =>
|
||||
const available = createMemo(() =>
|
||||
providers.connected().flatMap((p) =>
|
||||
Object.values(p.models).map((m) => ({
|
||||
...m,
|
||||
name: m.name.replace("(latest)", "").trim(),
|
||||
provider: p,
|
||||
latest: m.name.includes("(latest)"),
|
||||
})),
|
||||
),
|
||||
)
|
||||
|
||||
const latest = createMemo(() =>
|
||||
pipe(
|
||||
available(),
|
||||
filter((x) => Math.abs(DateTime.fromISO(x.release_date).diffNow().as("months")) < 6),
|
||||
groupBy((x) => x.provider.id),
|
||||
mapValues((models) =>
|
||||
pipe(
|
||||
models,
|
||||
groupBy((x) => x.family),
|
||||
values(),
|
||||
(groups) =>
|
||||
groups.flatMap((g) => {
|
||||
const first = firstBy(g, [(x) => x.release_date, "desc"])
|
||||
return first ? [{ modelID: first.id, providerID: first.provider.id }] : []
|
||||
}),
|
||||
),
|
||||
),
|
||||
values(),
|
||||
flat(),
|
||||
),
|
||||
)
|
||||
|
||||
const list = createMemo(() =>
|
||||
available().map((m) => ({
|
||||
...m,
|
||||
name: m.name.replace("(latest)", "").trim(),
|
||||
latest: m.name.includes("(latest)"),
|
||||
})),
|
||||
)
|
||||
|
||||
const find = (key: ModelKey) => list().find((m) => m.id === key?.modelID && m.provider.id === key.providerID)
|
||||
|
||||
const fallbackModel = createMemo(() => {
|
||||
@@ -163,10 +197,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
throw new Error("No default model found")
|
||||
})
|
||||
|
||||
const currentModel = createMemo(() => {
|
||||
const current = createMemo(() => {
|
||||
const a = agent.current()
|
||||
const key = getFirstValidModel(
|
||||
() => store.model[a.name],
|
||||
() => ephemeral.model[a.name],
|
||||
() => a.model,
|
||||
fallbackModel,
|
||||
)!
|
||||
@@ -177,10 +211,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
|
||||
const cycle = (direction: 1 | -1) => {
|
||||
const recentList = recent()
|
||||
const current = currentModel()
|
||||
if (!current) return
|
||||
const currentModel = current()
|
||||
if (!currentModel) return
|
||||
|
||||
const index = recentList.findIndex((x) => x?.provider.id === current.provider.id && x?.id === current.id)
|
||||
const index = recentList.findIndex(
|
||||
(x) => x?.provider.id === currentModel.provider.id && x?.id === currentModel.id,
|
||||
)
|
||||
if (index === -1) return
|
||||
|
||||
let next = index + direction
|
||||
@@ -196,14 +232,24 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
})
|
||||
}
|
||||
|
||||
function updateVisibility(model: ModelKey, visibility: "show" | "hide") {
|
||||
const index = store.user.findIndex((x) => x.modelID === model.modelID && x.providerID === model.providerID)
|
||||
if (index >= 0) {
|
||||
setStore("user", index, { visibility })
|
||||
} else {
|
||||
setStore("user", store.user.length, { ...model, visibility })
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
current: currentModel,
|
||||
current,
|
||||
recent,
|
||||
list,
|
||||
cycle,
|
||||
set(model: ModelKey | undefined, options?: { recent?: boolean }) {
|
||||
batch(() => {
|
||||
setStore("model", agent.current().name, model ?? fallbackModel())
|
||||
setEphemeral("model", agent.current().name, model ?? fallbackModel())
|
||||
if (model) updateVisibility(model, "show")
|
||||
if (options?.recent && model) {
|
||||
const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID)
|
||||
if (uniq.length > 5) uniq.pop()
|
||||
@@ -211,6 +257,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
}
|
||||
})
|
||||
},
|
||||
visible(model: ModelKey) {
|
||||
const user = store.user.find((x) => x.modelID === model.modelID && x.providerID === model.providerID)
|
||||
return (
|
||||
user?.visibility !== "hide" &&
|
||||
(latest().find((x) => x.modelID === model.modelID && x.providerID === model.providerID) ||
|
||||
user?.visibility === "show")
|
||||
)
|
||||
},
|
||||
setVisibility(model: ModelKey, visible: boolean) {
|
||||
updateVisibility(model, visible ? "show" : "hide")
|
||||
},
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -349,7 +406,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
case "file.watcher.updated":
|
||||
const relativePath = relative(event.properties.file)
|
||||
if (relativePath.startsWith(".git/")) return
|
||||
load(relativePath)
|
||||
if (store.node[relativePath]) load(relativePath)
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
124
packages/desktop/src/context/notification.tsx
Normal file
124
packages/desktop/src/context/notification.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { makePersisted } from "@solid-primitives/storage"
|
||||
import { useGlobalSDK } from "./global-sdk"
|
||||
import { useGlobalSync } from "./global-sync"
|
||||
import { Binary } from "@opencode-ai/util/binary"
|
||||
import { EventSessionError } from "@opencode-ai/sdk/v2"
|
||||
import { makeAudioPlayer } from "@solid-primitives/audio"
|
||||
import idleSound from "@opencode-ai/ui/audio/staplebops-01.aac"
|
||||
import errorSound from "@opencode-ai/ui/audio/nope-03.aac"
|
||||
|
||||
type NotificationBase = {
|
||||
directory?: string
|
||||
session?: string
|
||||
metadata?: any
|
||||
time: number
|
||||
viewed: boolean
|
||||
}
|
||||
|
||||
type TurnCompleteNotification = NotificationBase & {
|
||||
type: "turn-complete"
|
||||
}
|
||||
|
||||
type ErrorNotification = NotificationBase & {
|
||||
type: "error"
|
||||
error: EventSessionError["properties"]["error"]
|
||||
}
|
||||
|
||||
export type Notification = TurnCompleteNotification | ErrorNotification
|
||||
|
||||
export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({
|
||||
name: "Notification",
|
||||
init: () => {
|
||||
let idlePlayer: ReturnType<typeof makeAudioPlayer> | undefined
|
||||
let errorPlayer: ReturnType<typeof makeAudioPlayer> | undefined
|
||||
|
||||
try {
|
||||
idlePlayer = makeAudioPlayer(idleSound)
|
||||
errorPlayer = makeAudioPlayer(errorSound)
|
||||
} catch (err) {
|
||||
console.log("Failed to load audio", err)
|
||||
}
|
||||
|
||||
const globalSDK = useGlobalSDK()
|
||||
const globalSync = useGlobalSync()
|
||||
|
||||
const [store, setStore] = makePersisted(
|
||||
createStore({
|
||||
list: [] as Notification[],
|
||||
}),
|
||||
{
|
||||
name: "notification.v1",
|
||||
},
|
||||
)
|
||||
|
||||
globalSDK.event.listen((e) => {
|
||||
const directory = e.name
|
||||
const event = e.details
|
||||
const base = {
|
||||
directory,
|
||||
time: Date.now(),
|
||||
viewed: false,
|
||||
}
|
||||
switch (event.type) {
|
||||
case "session.idle": {
|
||||
const sessionID = event.properties.sessionID
|
||||
const [syncStore] = globalSync.child(directory)
|
||||
const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
|
||||
const isChild = match.found && syncStore.session[match.index].parentID
|
||||
if (isChild) break
|
||||
idlePlayer?.play()
|
||||
setStore("list", store.list.length, {
|
||||
...base,
|
||||
type: "turn-complete",
|
||||
session: sessionID,
|
||||
})
|
||||
break
|
||||
}
|
||||
case "session.error": {
|
||||
const sessionID = event.properties.sessionID
|
||||
if (sessionID) {
|
||||
const [syncStore] = globalSync.child(directory)
|
||||
const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
|
||||
const isChild = match.found && syncStore.session[match.index].parentID
|
||||
if (isChild) break
|
||||
}
|
||||
errorPlayer?.play()
|
||||
setStore("list", store.list.length, {
|
||||
...base,
|
||||
type: "error",
|
||||
session: sessionID ?? "global",
|
||||
error: "error" in event.properties ? event.properties.error : undefined,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
session: {
|
||||
all(session: string) {
|
||||
return store.list.filter((n) => n.session === session)
|
||||
},
|
||||
unseen(session: string) {
|
||||
return store.list.filter((n) => n.session === session && !n.viewed)
|
||||
},
|
||||
markViewed(session: string) {
|
||||
setStore("list", (n) => n.session === session, "viewed", true)
|
||||
},
|
||||
},
|
||||
project: {
|
||||
all(directory: string) {
|
||||
return store.list.filter((n) => n.directory === directory)
|
||||
},
|
||||
unseen(directory: string) {
|
||||
return store.list.filter((n) => n.directory === directory && !n.viewed)
|
||||
},
|
||||
markViewed(directory: string) {
|
||||
setStore("list", (n) => n.directory === directory, "viewed", true)
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
112
packages/desktop/src/context/prompt.tsx
Normal file
112
packages/desktop/src/context/prompt.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { batch, createMemo } from "solid-js"
|
||||
import { makePersisted } from "@solid-primitives/storage"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { TextSelection } from "./local"
|
||||
|
||||
interface PartBase {
|
||||
content: string
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
export interface TextPart extends PartBase {
|
||||
type: "text"
|
||||
}
|
||||
|
||||
export interface FileAttachmentPart extends PartBase {
|
||||
type: "file"
|
||||
path: string
|
||||
selection?: TextSelection
|
||||
}
|
||||
|
||||
export interface ImageAttachmentPart {
|
||||
type: "image"
|
||||
id: string
|
||||
filename: string
|
||||
mime: string
|
||||
dataUrl: string
|
||||
}
|
||||
|
||||
export type ContentPart = TextPart | FileAttachmentPart | ImageAttachmentPart
|
||||
export type Prompt = ContentPart[]
|
||||
|
||||
export const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
|
||||
|
||||
export function isPromptEqual(promptA: Prompt, promptB: Prompt): boolean {
|
||||
if (promptA.length !== promptB.length) return false
|
||||
for (let i = 0; i < promptA.length; i++) {
|
||||
const partA = promptA[i]
|
||||
const partB = promptB[i]
|
||||
if (partA.type !== partB.type) return false
|
||||
if (partA.type === "text" && partA.content !== (partB as TextPart).content) {
|
||||
return false
|
||||
}
|
||||
if (partA.type === "file" && partA.path !== (partB as FileAttachmentPart).path) {
|
||||
return false
|
||||
}
|
||||
if (partA.type === "image" && partA.id !== (partB as ImageAttachmentPart).id) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function cloneSelection(selection?: TextSelection) {
|
||||
if (!selection) return undefined
|
||||
return { ...selection }
|
||||
}
|
||||
|
||||
function clonePart(part: ContentPart): ContentPart {
|
||||
if (part.type === "text") return { ...part }
|
||||
if (part.type === "image") return { ...part }
|
||||
return {
|
||||
...part,
|
||||
selection: cloneSelection(part.selection),
|
||||
}
|
||||
}
|
||||
|
||||
function clonePrompt(prompt: Prompt): Prompt {
|
||||
return prompt.map(clonePart)
|
||||
}
|
||||
|
||||
export const { use: usePrompt, provider: PromptProvider } = createSimpleContext({
|
||||
name: "Prompt",
|
||||
init: () => {
|
||||
const params = useParams()
|
||||
const name = createMemo(() => `${params.dir}/prompt${params.id ? "/" + params.id : ""}.v1`)
|
||||
|
||||
const [store, setStore] = makePersisted(
|
||||
createStore<{
|
||||
prompt: Prompt
|
||||
cursor?: number
|
||||
}>({
|
||||
prompt: clonePrompt(DEFAULT_PROMPT),
|
||||
cursor: undefined,
|
||||
}),
|
||||
{
|
||||
name: name(),
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
current: createMemo(() => store.prompt),
|
||||
cursor: createMemo(() => store.cursor),
|
||||
dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
|
||||
set(prompt: Prompt, cursorPosition?: number) {
|
||||
const next = clonePrompt(prompt)
|
||||
batch(() => {
|
||||
setStore("prompt", next)
|
||||
if (cursorPosition !== undefined) setStore("cursor", cursorPosition)
|
||||
})
|
||||
},
|
||||
reset() {
|
||||
batch(() => {
|
||||
setStore("prompt", clonePrompt(DEFAULT_PROMPT))
|
||||
setStore("cursor", 0)
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,321 +0,0 @@
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { batch, createEffect, createMemo } from "solid-js"
|
||||
import { useSync } from "./sync"
|
||||
import { makePersisted } from "@solid-primitives/storage"
|
||||
import { TextSelection } from "./local"
|
||||
import { pipe, sumBy } from "remeda"
|
||||
import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { useSDK } from "./sdk"
|
||||
|
||||
export type LocalPTY = {
|
||||
id: string
|
||||
title: string
|
||||
rows?: number
|
||||
cols?: number
|
||||
buffer?: string
|
||||
scrollY?: number
|
||||
}
|
||||
|
||||
export const { use: useSession, provider: SessionProvider } = createSimpleContext({
|
||||
name: "Session",
|
||||
init: () => {
|
||||
const sdk = useSDK()
|
||||
const params = useParams()
|
||||
const sync = useSync()
|
||||
const name = createMemo(() => `${params.dir}/session${params.id ? "/" + params.id : ""}.v3`)
|
||||
|
||||
const [store, setStore] = makePersisted(
|
||||
createStore<{
|
||||
messageId?: string
|
||||
tabs: {
|
||||
active?: string
|
||||
all: string[]
|
||||
}
|
||||
prompt: Prompt
|
||||
cursor?: number
|
||||
terminals: {
|
||||
active?: string
|
||||
all: LocalPTY[]
|
||||
}
|
||||
}>({
|
||||
tabs: {
|
||||
all: [],
|
||||
},
|
||||
prompt: clonePrompt(DEFAULT_PROMPT),
|
||||
cursor: undefined,
|
||||
terminals: { all: [] },
|
||||
}),
|
||||
{
|
||||
name: name(),
|
||||
},
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
if (!params.id) return
|
||||
sync.session.sync(params.id)
|
||||
})
|
||||
|
||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
||||
const userMessages = createMemo(() =>
|
||||
messages()
|
||||
.filter((m) => m.role === "user")
|
||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
||||
)
|
||||
const lastUserMessage = createMemo(() => {
|
||||
return userMessages()?.at(-1)
|
||||
})
|
||||
const activeMessage = createMemo(() => {
|
||||
if (!store.messageId) return lastUserMessage()
|
||||
return userMessages()?.find((m) => m.id === store.messageId)
|
||||
})
|
||||
const status = createMemo(
|
||||
() =>
|
||||
sync.data.session_status[params.id ?? ""] ?? {
|
||||
type: "idle",
|
||||
},
|
||||
)
|
||||
const working = createMemo(() => status()?.type !== "idle")
|
||||
|
||||
const cost = createMemo(() => {
|
||||
const total = pipe(
|
||||
messages(),
|
||||
sumBy((x) => (x.role === "assistant" ? x.cost : 0)),
|
||||
)
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
}).format(total)
|
||||
})
|
||||
|
||||
const last = createMemo(
|
||||
() => messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage,
|
||||
)
|
||||
const model = createMemo(() =>
|
||||
last() ? sync.data.provider.all.find((x) => x.id === last().providerID)?.models[last().modelID] : undefined,
|
||||
)
|
||||
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
|
||||
|
||||
const tokens = createMemo(() => {
|
||||
if (!last()) return
|
||||
const tokens = last().tokens
|
||||
return tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write
|
||||
})
|
||||
|
||||
const context = createMemo(() => {
|
||||
const total = tokens()
|
||||
const limit = model()?.limit.context
|
||||
if (!total || !limit) return 0
|
||||
return Math.round((total / limit) * 100)
|
||||
})
|
||||
|
||||
return {
|
||||
get id() {
|
||||
return params.id
|
||||
},
|
||||
info,
|
||||
status,
|
||||
working,
|
||||
diffs,
|
||||
prompt: {
|
||||
current: createMemo(() => store.prompt),
|
||||
cursor: createMemo(() => store.cursor),
|
||||
dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
|
||||
set(prompt: Prompt, cursorPosition?: number) {
|
||||
const next = clonePrompt(prompt)
|
||||
batch(() => {
|
||||
setStore("prompt", next)
|
||||
if (cursorPosition !== undefined) setStore("cursor", cursorPosition)
|
||||
})
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
all: messages,
|
||||
user: userMessages,
|
||||
last: lastUserMessage,
|
||||
active: activeMessage,
|
||||
setActive(message: UserMessage | undefined) {
|
||||
setStore("messageId", message?.id)
|
||||
},
|
||||
},
|
||||
usage: {
|
||||
tokens,
|
||||
cost,
|
||||
context,
|
||||
},
|
||||
layout: {
|
||||
tabs: store.tabs,
|
||||
setActiveTab(tab: string | undefined) {
|
||||
setStore("tabs", "active", tab)
|
||||
},
|
||||
setOpenedTabs(tabs: string[]) {
|
||||
setStore("tabs", "all", tabs)
|
||||
},
|
||||
async openTab(tab: string) {
|
||||
if (tab === "chat") {
|
||||
setStore("tabs", "active", undefined)
|
||||
return
|
||||
}
|
||||
if (tab !== "review") {
|
||||
if (!store.tabs.all.includes(tab)) {
|
||||
setStore("tabs", "all", [...store.tabs.all, tab])
|
||||
}
|
||||
}
|
||||
setStore("tabs", "active", tab)
|
||||
},
|
||||
closeTab(tab: string) {
|
||||
batch(() => {
|
||||
setStore(
|
||||
"tabs",
|
||||
"all",
|
||||
store.tabs.all.filter((x) => x !== tab),
|
||||
)
|
||||
if (store.tabs.active === tab) {
|
||||
const index = store.tabs.all.findIndex((f) => f === tab)
|
||||
const previous = store.tabs.all[Math.max(0, index - 1)]
|
||||
setStore("tabs", "active", previous)
|
||||
}
|
||||
})
|
||||
},
|
||||
moveTab(tab: string, to: number) {
|
||||
const index = store.tabs.all.findIndex((f) => f === tab)
|
||||
if (index === -1) return
|
||||
setStore(
|
||||
"tabs",
|
||||
"all",
|
||||
produce((opened) => {
|
||||
opened.splice(to, 0, opened.splice(index, 1)[0])
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
terminal: {
|
||||
all: createMemo(() => Object.values(store.terminals.all)),
|
||||
active: createMemo(() => store.terminals.active),
|
||||
new() {
|
||||
sdk.client.pty.create({ title: `Terminal ${store.terminals.all.length + 1}` }).then((pty) => {
|
||||
const id = pty.data?.id
|
||||
if (!id) return
|
||||
setStore("terminals", "all", [
|
||||
...store.terminals.all,
|
||||
{
|
||||
id,
|
||||
title: pty.data?.title ?? "Terminal",
|
||||
},
|
||||
])
|
||||
setStore("terminals", "active", id)
|
||||
})
|
||||
},
|
||||
update(pty: Partial<LocalPTY> & { id: string }) {
|
||||
setStore("terminals", "all", (x) => x.map((x) => (x.id === pty.id ? { ...x, ...pty } : x)))
|
||||
sdk.client.pty.update({
|
||||
ptyID: pty.id,
|
||||
title: pty.title,
|
||||
size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
|
||||
})
|
||||
},
|
||||
async clone(id: string) {
|
||||
const index = store.terminals.all.findIndex((x) => x.id === id)
|
||||
const pty = store.terminals.all[index]
|
||||
if (!pty) return
|
||||
const clone = await sdk.client.pty.create({
|
||||
title: pty.title,
|
||||
})
|
||||
if (!clone.data) return
|
||||
setStore("terminals", "all", index, {
|
||||
...pty,
|
||||
...clone.data,
|
||||
})
|
||||
if (store.terminals.active === pty.id) {
|
||||
setStore("terminals", "active", clone.data.id)
|
||||
}
|
||||
},
|
||||
open(id: string) {
|
||||
setStore("terminals", "active", id)
|
||||
},
|
||||
async close(id: string) {
|
||||
batch(() => {
|
||||
setStore(
|
||||
"terminals",
|
||||
"all",
|
||||
store.terminals.all.filter((x) => x.id !== id),
|
||||
)
|
||||
if (store.terminals.active === id) {
|
||||
const index = store.terminals.all.findIndex((f) => f.id === id)
|
||||
const previous = store.tabs.all[Math.max(0, index - 1)]
|
||||
setStore("terminals", "active", previous)
|
||||
}
|
||||
})
|
||||
await sdk.client.pty.remove({ ptyID: id })
|
||||
},
|
||||
move(id: string, to: number) {
|
||||
const index = store.terminals.all.findIndex((f) => f.id === id)
|
||||
if (index === -1) return
|
||||
setStore(
|
||||
"terminals",
|
||||
"all",
|
||||
produce((all) => {
|
||||
all.splice(to, 0, all.splice(index, 1)[0])
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
interface PartBase {
|
||||
content: string
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
export interface TextPart extends PartBase {
|
||||
type: "text"
|
||||
}
|
||||
|
||||
export interface FileAttachmentPart extends PartBase {
|
||||
type: "file"
|
||||
path: string
|
||||
selection?: TextSelection
|
||||
}
|
||||
|
||||
export type ContentPart = TextPart | FileAttachmentPart
|
||||
export type Prompt = ContentPart[]
|
||||
|
||||
export const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
|
||||
|
||||
export function isPromptEqual(promptA: Prompt, promptB: Prompt): boolean {
|
||||
if (promptA.length !== promptB.length) return false
|
||||
for (let i = 0; i < promptA.length; i++) {
|
||||
const partA = promptA[i]
|
||||
const partB = promptB[i]
|
||||
if (partA.type !== partB.type) return false
|
||||
if (partA.type === "text" && partA.content !== (partB as TextPart).content) {
|
||||
return false
|
||||
}
|
||||
if (partA.type === "file" && partA.path !== (partB as FileAttachmentPart).path) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function cloneSelection(selection?: TextSelection) {
|
||||
if (!selection) return undefined
|
||||
return { ...selection }
|
||||
}
|
||||
|
||||
function clonePart(part: ContentPart): ContentPart {
|
||||
if (part.type === "text") return { ...part }
|
||||
return {
|
||||
...part,
|
||||
selection: cloneSelection(part.selection),
|
||||
}
|
||||
}
|
||||
|
||||
function clonePrompt(prompt: Prompt): Prompt {
|
||||
return prompt.map(clonePart)
|
||||
}
|
||||
@@ -65,6 +65,15 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
})
|
||||
},
|
||||
more: createMemo(() => store.session.length >= store.limit),
|
||||
archive: async (sessionID: string) => {
|
||||
await sdk.client.session.update({ sessionID, time: { archived: Date.now() } })
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
const match = Binary.search(draft.session, sessionID, (s) => s.id)
|
||||
if (match.found) draft.session.splice(match.index, 1)
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
absolute,
|
||||
get directory() {
|
||||
|
||||
106
packages/desktop/src/context/terminal.tsx
Normal file
106
packages/desktop/src/context/terminal.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { batch, createMemo } from "solid-js"
|
||||
import { makePersisted } from "@solid-primitives/storage"
|
||||
import { useParams } from "@solidjs/router"
|
||||
import { useSDK } from "./sdk"
|
||||
|
||||
export type LocalPTY = {
|
||||
id: string
|
||||
title: string
|
||||
rows?: number
|
||||
cols?: number
|
||||
buffer?: string
|
||||
scrollY?: number
|
||||
}
|
||||
|
||||
export const { use: useTerminal, provider: TerminalProvider } = createSimpleContext({
|
||||
name: "Terminal",
|
||||
init: () => {
|
||||
const sdk = useSDK()
|
||||
const params = useParams()
|
||||
const name = createMemo(() => `${params.dir}/terminal${params.id ? "/" + params.id : ""}.v1`)
|
||||
|
||||
const [store, setStore] = makePersisted(
|
||||
createStore<{
|
||||
active?: string
|
||||
all: LocalPTY[]
|
||||
}>({
|
||||
all: [],
|
||||
}),
|
||||
{
|
||||
name: name(),
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
all: createMemo(() => Object.values(store.all)),
|
||||
active: createMemo(() => store.active),
|
||||
new() {
|
||||
sdk.client.pty.create({ title: `Terminal ${store.all.length + 1}` }).then((pty) => {
|
||||
const id = pty.data?.id
|
||||
if (!id) return
|
||||
setStore("all", [
|
||||
...store.all,
|
||||
{
|
||||
id,
|
||||
title: pty.data?.title ?? "Terminal",
|
||||
},
|
||||
])
|
||||
setStore("active", id)
|
||||
})
|
||||
},
|
||||
update(pty: Partial<LocalPTY> & { id: string }) {
|
||||
setStore("all", (x) => x.map((x) => (x.id === pty.id ? { ...x, ...pty } : x)))
|
||||
sdk.client.pty.update({
|
||||
ptyID: pty.id,
|
||||
title: pty.title,
|
||||
size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
|
||||
})
|
||||
},
|
||||
async clone(id: string) {
|
||||
const index = store.all.findIndex((x) => x.id === id)
|
||||
const pty = store.all[index]
|
||||
if (!pty) return
|
||||
const clone = await sdk.client.pty.create({
|
||||
title: pty.title,
|
||||
})
|
||||
if (!clone.data) return
|
||||
setStore("all", index, {
|
||||
...pty,
|
||||
...clone.data,
|
||||
})
|
||||
if (store.active === pty.id) {
|
||||
setStore("active", clone.data.id)
|
||||
}
|
||||
},
|
||||
open(id: string) {
|
||||
setStore("active", id)
|
||||
},
|
||||
async close(id: string) {
|
||||
batch(() => {
|
||||
setStore(
|
||||
"all",
|
||||
store.all.filter((x) => x.id !== id),
|
||||
)
|
||||
if (store.active === id) {
|
||||
const index = store.all.findIndex((f) => f.id === id)
|
||||
const previous = store.all[Math.max(0, index - 1)]
|
||||
setStore("active", previous?.id)
|
||||
}
|
||||
})
|
||||
await sdk.client.pty.remove({ ptyID: id })
|
||||
},
|
||||
move(id: string, to: number) {
|
||||
const index = store.all.findIndex((f) => f.id === id)
|
||||
if (index === -1) return
|
||||
setStore(
|
||||
"all",
|
||||
produce((all) => {
|
||||
all.splice(to, 0, all.splice(index, 1)[0])
|
||||
}),
|
||||
)
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -6,8 +6,8 @@ import { createMemo } from "solid-js"
|
||||
export const popularProviders = ["opencode", "anthropic", "github-copilot", "openai", "google", "openrouter", "vercel"]
|
||||
|
||||
export function useProviders() {
|
||||
const params = useParams()
|
||||
const globalSync = useGlobalSync()
|
||||
const params = useParams()
|
||||
const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
|
||||
const providers = createMemo(() => {
|
||||
if (currentDirectory()) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { For, Match, Show, Switch } from "solid-js"
|
||||
import { createMemo, For, Match, Show, Switch } from "solid-js"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Logo } from "@opencode-ai/ui/logo"
|
||||
import { useLayout } from "@/context/layout"
|
||||
@@ -14,6 +14,7 @@ export default function Home() {
|
||||
const layout = useLayout()
|
||||
const platform = usePlatform()
|
||||
const navigate = useNavigate()
|
||||
const homedir = createMemo(() => sync.data.path.home)
|
||||
|
||||
function openProject(directory: string) {
|
||||
layout.projects.open(directory)
|
||||
@@ -61,7 +62,7 @@ export default function Home() {
|
||||
class="text-14-mono text-left justify-between px-3"
|
||||
onClick={() => openProject(project.worktree)}
|
||||
>
|
||||
{project.worktree}
|
||||
{project.worktree.replace(homedir(), "~")}
|
||||
<div class="text-14-regular text-text-weak">
|
||||
{DateTime.fromMillis(project.time.updated ?? project.time.created).toRelative()}
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
import { For, onCleanup, onMount, Show, Match, Switch, createResource, createMemo, createEffect } from "solid-js"
|
||||
import { For, onCleanup, onMount, Show, Match, Switch, createResource, createMemo, createEffect, on } from "solid-js"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
import { useLocal, type LocalFile } from "@/context/local"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { PromptInput } from "@/components/prompt-input"
|
||||
@@ -11,11 +12,10 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
||||
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
|
||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { Code } from "@opencode-ai/ui/code"
|
||||
import { useCodeComponent } from "@opencode-ai/ui/context/code"
|
||||
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
||||
import { SessionMessageRail } from "@opencode-ai/ui/session-message-rail"
|
||||
import { SessionReview } from "@opencode-ai/ui/session-review"
|
||||
import { SelectDialog } from "@opencode-ai/ui/select-dialog"
|
||||
import {
|
||||
DragDropProvider,
|
||||
DragDropSensors,
|
||||
@@ -28,26 +28,325 @@ import {
|
||||
import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
|
||||
import type { JSX } from "solid-js"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useSession, type LocalPTY } from "@/context/session"
|
||||
import { useTerminal, type LocalPTY } from "@/context/terminal"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { Terminal } from "@/components/terminal"
|
||||
import { checksum } from "@opencode-ai/util/encode"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { DialogSelectFile } from "@/components/dialog-select-file"
|
||||
import { DialogSelectModel } from "@/components/dialog-select-model"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { useNavigate, useParams } from "@solidjs/router"
|
||||
import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { useSDK } from "@/context/sdk"
|
||||
import { usePrompt } from "@/context/prompt"
|
||||
import { extractPromptFromParts } from "@/utils/prompt"
|
||||
|
||||
export default function Page() {
|
||||
const layout = useLayout()
|
||||
const local = useLocal()
|
||||
const sync = useSync()
|
||||
const session = useSession()
|
||||
const terminal = useTerminal()
|
||||
const dialog = useDialog()
|
||||
const codeComponent = useCodeComponent()
|
||||
const command = useCommand()
|
||||
const params = useParams()
|
||||
const navigate = useNavigate()
|
||||
const sdk = useSDK()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey()))
|
||||
|
||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||
const revertMessageID = createMemo(() => info()?.revert?.messageID)
|
||||
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
||||
const userMessages = createMemo(() =>
|
||||
messages()
|
||||
.filter((m) => m.role === "user")
|
||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
||||
)
|
||||
// Visible user messages excludes reverted messages (those >= revertMessageID)
|
||||
const visibleUserMessages = createMemo(() => {
|
||||
const revert = revertMessageID()
|
||||
if (!revert) return userMessages()
|
||||
return userMessages().filter((m) => m.id < revert)
|
||||
})
|
||||
const lastUserMessage = createMemo(() => visibleUserMessages()?.at(-1))
|
||||
|
||||
const [messageStore, setMessageStore] = createStore<{ messageId?: string }>({})
|
||||
const activeMessage = createMemo(() => {
|
||||
if (!messageStore.messageId) return lastUserMessage()
|
||||
// If the stored message is no longer visible (e.g., was reverted), fall back to last visible
|
||||
const found = visibleUserMessages()?.find((m) => m.id === messageStore.messageId)
|
||||
return found ?? lastUserMessage()
|
||||
})
|
||||
const setActiveMessage = (message: UserMessage | undefined) => {
|
||||
setMessageStore("messageId", message?.id)
|
||||
}
|
||||
|
||||
function navigateMessageByOffset(offset: number) {
|
||||
const msgs = visibleUserMessages()
|
||||
if (msgs.length === 0) return
|
||||
|
||||
const current = activeMessage()
|
||||
const currentIndex = current ? msgs.findIndex((m) => m.id === current.id) : -1
|
||||
|
||||
let targetIndex: number
|
||||
if (currentIndex === -1) {
|
||||
targetIndex = offset > 0 ? 0 : msgs.length - 1
|
||||
} else {
|
||||
targetIndex = currentIndex + offset
|
||||
}
|
||||
|
||||
if (targetIndex < 0 || targetIndex >= msgs.length) return
|
||||
|
||||
setActiveMessage(msgs[targetIndex])
|
||||
}
|
||||
|
||||
const last = createMemo(
|
||||
() => messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage,
|
||||
)
|
||||
const model = createMemo(() =>
|
||||
last() ? sync.data.provider.all.find((x) => x.id === last().providerID)?.models[last().modelID] : undefined,
|
||||
)
|
||||
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
|
||||
|
||||
const tokens = createMemo(() => {
|
||||
if (!last()) return
|
||||
const t = last().tokens
|
||||
return t.input + t.output + t.reasoning + t.cache.read + t.cache.write
|
||||
})
|
||||
|
||||
const context = createMemo(() => {
|
||||
const total = tokens()
|
||||
const limit = model()?.limit.context
|
||||
if (!total || !limit) return 0
|
||||
return Math.round((total / limit) * 100)
|
||||
})
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
clickTimer: undefined as number | undefined,
|
||||
fileSelectOpen: false,
|
||||
activeDraggable: undefined as string | undefined,
|
||||
activeTerminalDraggable: undefined as string | undefined,
|
||||
stepsExpanded: false,
|
||||
})
|
||||
let inputRef!: HTMLDivElement
|
||||
|
||||
const MOD = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) ? "Meta" : "Control"
|
||||
createEffect(() => {
|
||||
if (!params.id) return
|
||||
sync.session.sync(params.id)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (layout.terminal.opened()) {
|
||||
if (terminal.all().length === 0) {
|
||||
terminal.new()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => visibleUserMessages().at(-1)?.id,
|
||||
(lastId, prevLastId) => {
|
||||
if (lastId && prevLastId && lastId > prevLastId) {
|
||||
setMessageStore("messageId", undefined)
|
||||
}
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? { type: "idle" })
|
||||
|
||||
command.register(() => [
|
||||
{
|
||||
id: "session.new",
|
||||
title: "New session",
|
||||
description: "Create a new session",
|
||||
category: "Session",
|
||||
keybind: "mod+shift+s",
|
||||
slash: "new",
|
||||
onSelect: () => navigate(`/${params.dir}/session`),
|
||||
},
|
||||
{
|
||||
id: "file.open",
|
||||
title: "Open file",
|
||||
description: "Search and open a file",
|
||||
category: "File",
|
||||
keybind: "mod+p",
|
||||
slash: "open",
|
||||
onSelect: () => dialog.show(() => <DialogSelectFile />),
|
||||
},
|
||||
// {
|
||||
// id: "theme.toggle",
|
||||
// title: "Toggle theme",
|
||||
// description: "Switch between themes",
|
||||
// category: "View",
|
||||
// keybind: "ctrl+t",
|
||||
// slash: "theme",
|
||||
// onSelect: () => {
|
||||
// const currentTheme = localStorage.getItem("theme") ?? "oc-1"
|
||||
// const themes = ["oc-1", "oc-2-paper"]
|
||||
// const nextTheme = themes[(themes.indexOf(currentTheme) + 1) % themes.length]
|
||||
// localStorage.setItem("theme", nextTheme)
|
||||
// document.documentElement.setAttribute("data-theme", nextTheme)
|
||||
// },
|
||||
// },
|
||||
{
|
||||
id: "terminal.toggle",
|
||||
title: "Toggle terminal",
|
||||
description: "Show or hide the terminal",
|
||||
category: "View",
|
||||
keybind: "ctrl+`",
|
||||
slash: "terminal",
|
||||
onSelect: () => layout.terminal.toggle(),
|
||||
},
|
||||
{
|
||||
id: "terminal.new",
|
||||
title: "New terminal",
|
||||
description: "Create a new terminal tab",
|
||||
category: "Terminal",
|
||||
keybind: "ctrl+shift+`",
|
||||
onSelect: () => terminal.new(),
|
||||
},
|
||||
{
|
||||
id: "steps.toggle",
|
||||
title: "Toggle steps",
|
||||
description: "Show or hide the steps",
|
||||
category: "View",
|
||||
keybind: "mod+e",
|
||||
slash: "steps",
|
||||
disabled: !params.id,
|
||||
onSelect: () => setStore("stepsExpanded", (x) => !x),
|
||||
},
|
||||
{
|
||||
id: "message.previous",
|
||||
title: "Previous message",
|
||||
description: "Go to the previous user message",
|
||||
category: "Session",
|
||||
keybind: "mod+arrowup",
|
||||
disabled: !params.id,
|
||||
onSelect: () => navigateMessageByOffset(-1),
|
||||
},
|
||||
{
|
||||
id: "message.next",
|
||||
title: "Next message",
|
||||
description: "Go to the next user message",
|
||||
category: "Session",
|
||||
keybind: "mod+arrowdown",
|
||||
disabled: !params.id,
|
||||
onSelect: () => navigateMessageByOffset(1),
|
||||
},
|
||||
{
|
||||
id: "model.choose",
|
||||
title: "Choose model",
|
||||
description: "Select a different model",
|
||||
category: "Model",
|
||||
keybind: "mod+'",
|
||||
slash: "model",
|
||||
onSelect: () => dialog.show(() => <DialogSelectModel />),
|
||||
},
|
||||
{
|
||||
id: "agent.cycle",
|
||||
title: "Cycle agent",
|
||||
description: "Switch to the next agent",
|
||||
category: "Agent",
|
||||
keybind: "mod+.",
|
||||
slash: "agent",
|
||||
onSelect: () => local.agent.move(1),
|
||||
},
|
||||
{
|
||||
id: "session.undo",
|
||||
title: "Undo",
|
||||
description: "Undo the last message",
|
||||
category: "Session",
|
||||
keybind: "mod+z",
|
||||
slash: "undo",
|
||||
disabled: !params.id || visibleUserMessages().length === 0,
|
||||
onSelect: async () => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
if (status()?.type !== "idle") {
|
||||
await sdk.client.session.abort({ sessionID }).catch(() => {})
|
||||
}
|
||||
const revert = info()?.revert?.messageID
|
||||
// Find the last user message that's not already reverted
|
||||
const message = userMessages().findLast((x) => !revert || x.id < revert)
|
||||
if (!message) return
|
||||
await sdk.client.session.revert({ sessionID, messageID: message.id })
|
||||
// Restore the prompt from the reverted message
|
||||
const parts = sync.data.part[message.id]
|
||||
if (parts) {
|
||||
const restored = extractPromptFromParts(parts)
|
||||
prompt.set(restored)
|
||||
}
|
||||
// Navigate to the message before the reverted one (which will be the new last visible message)
|
||||
const priorMessage = userMessages().findLast((x) => x.id < message.id)
|
||||
setActiveMessage(priorMessage)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "session.redo",
|
||||
title: "Redo",
|
||||
description: "Redo the last undone message",
|
||||
category: "Session",
|
||||
keybind: "mod+shift+z",
|
||||
slash: "redo",
|
||||
disabled: !params.id || !info()?.revert?.messageID,
|
||||
onSelect: async () => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
const revertMessageID = info()?.revert?.messageID
|
||||
if (!revertMessageID) return
|
||||
const nextMessage = userMessages().find((x) => x.id > revertMessageID)
|
||||
if (!nextMessage) {
|
||||
// Full unrevert - restore all messages and navigate to last
|
||||
await sdk.client.session.unrevert({ sessionID })
|
||||
prompt.reset()
|
||||
// Navigate to the last message (the one that was at the revert point)
|
||||
const lastMsg = userMessages().findLast((x) => x.id >= revertMessageID)
|
||||
setActiveMessage(lastMsg)
|
||||
return
|
||||
}
|
||||
// Partial redo - move forward to next message
|
||||
await sdk.client.session.revert({ sessionID, messageID: nextMessage.id })
|
||||
// Navigate to the message before the new revert point
|
||||
const priorMsg = userMessages().findLast((x) => x.id < nextMessage.id)
|
||||
setActiveMessage(priorMsg)
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if ((document.activeElement as HTMLElement)?.dataset?.component === "terminal") return
|
||||
if (dialog.active) return
|
||||
|
||||
if (event.key === "PageUp" || event.key === "PageDown") {
|
||||
const scrollContainer = document.querySelector('[data-slot="session-turn-content"]') as HTMLElement
|
||||
if (scrollContainer) {
|
||||
event.preventDefault()
|
||||
const scrollAmount = scrollContainer.clientHeight * 0.8
|
||||
scrollContainer.scrollBy({
|
||||
top: event.key === "PageUp" ? -scrollAmount : scrollAmount,
|
||||
behavior: "instant",
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const focused = document.activeElement === inputRef
|
||||
if (focused) {
|
||||
if (event.key === "Escape") inputRef?.blur()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key.length === 1 && event.key !== "Unidentified" && !(event.ctrlKey || event.metaKey)) {
|
||||
inputRef?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener("keydown", handleKeyDown)
|
||||
@@ -57,82 +356,6 @@ export default function Page() {
|
||||
document.removeEventListener("keydown", handleKeyDown)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (layout.terminal.opened()) {
|
||||
if (session.terminal.all().length === 0) {
|
||||
session.terminal.new()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.getModifierState(MOD) && event.shiftKey && event.key.toLowerCase() === "p") {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
if (event.getModifierState(MOD) && event.key.toLowerCase() === "p") {
|
||||
event.preventDefault()
|
||||
setStore("fileSelectOpen", true)
|
||||
return
|
||||
}
|
||||
if (event.ctrlKey && event.key.toLowerCase() === "t") {
|
||||
event.preventDefault()
|
||||
const currentTheme = localStorage.getItem("theme") ?? "oc-1"
|
||||
const themes = ["oc-1", "oc-2-paper"]
|
||||
const nextTheme = themes[(themes.indexOf(currentTheme) + 1) % themes.length]
|
||||
localStorage.setItem("theme", nextTheme)
|
||||
document.documentElement.setAttribute("data-theme", nextTheme)
|
||||
return
|
||||
}
|
||||
if (event.ctrlKey && event.key.toLowerCase() === "`") {
|
||||
event.preventDefault()
|
||||
if (event.shiftKey) {
|
||||
session.terminal.new()
|
||||
return
|
||||
}
|
||||
layout.terminal.toggle()
|
||||
return
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
if (document.activeElement?.dataset?.component === "terminal") {
|
||||
return
|
||||
}
|
||||
|
||||
const focused = document.activeElement === inputRef
|
||||
if (focused) {
|
||||
if (event.key === "Escape") {
|
||||
inputRef?.blur()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// if (local.file.active()) {
|
||||
// const active = local.file.active()!
|
||||
// if (event.key === "Enter" && active.selection) {
|
||||
// local.context.add({
|
||||
// type: "file",
|
||||
// path: active.path,
|
||||
// selection: { ...active.selection },
|
||||
// })
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// if (event.getModifierState(MOD)) {
|
||||
// if (event.key.toLowerCase() === "a") {
|
||||
// return
|
||||
// }
|
||||
// if (event.key.toLowerCase() === "c") {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
if (event.key.length === 1 && event.key !== "Unidentified" && !(event.ctrlKey || event.metaKey)) {
|
||||
inputRef?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const resetClickTimer = () => {
|
||||
if (!store.clickTimer) return
|
||||
clearTimeout(store.clickTimer)
|
||||
@@ -166,11 +389,11 @@ export default function Page() {
|
||||
const handleDragOver = (event: DragEvent) => {
|
||||
const { draggable, droppable } = event
|
||||
if (draggable && droppable) {
|
||||
const currentTabs = session.layout.tabs.all
|
||||
const currentTabs = tabs().all()
|
||||
const fromIndex = currentTabs?.indexOf(draggable.id.toString())
|
||||
const toIndex = currentTabs?.indexOf(droppable.id.toString())
|
||||
if (fromIndex !== toIndex && toIndex !== undefined) {
|
||||
session.layout.moveTab(draggable.id.toString(), toIndex)
|
||||
tabs().move(draggable.id.toString(), toIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,11 +411,11 @@ export default function Page() {
|
||||
const handleTerminalDragOver = (event: DragEvent) => {
|
||||
const { draggable, droppable } = event
|
||||
if (draggable && droppable) {
|
||||
const terminals = session.terminal.all()
|
||||
const fromIndex = terminals.findIndex((t) => t.id === draggable.id.toString())
|
||||
const toIndex = terminals.findIndex((t) => t.id === droppable.id.toString())
|
||||
const terminals = terminal.all()
|
||||
const fromIndex = terminals.findIndex((t: LocalPTY) => t.id === draggable.id.toString())
|
||||
const toIndex = terminals.findIndex((t: LocalPTY) => t.id === droppable.id.toString())
|
||||
if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
|
||||
session.terminal.move(draggable.id.toString(), toIndex)
|
||||
terminal.move(draggable.id.toString(), toIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,8 +433,8 @@ export default function Page() {
|
||||
<Tabs.Trigger
|
||||
value={props.terminal.id}
|
||||
closeButton={
|
||||
session.terminal.all().length > 1 && (
|
||||
<IconButton icon="close" variant="ghost" onClick={() => session.terminal.close(props.terminal.id)} />
|
||||
terminal.all().length > 1 && (
|
||||
<IconButton icon="close" variant="ghost" onClick={() => terminal.close(props.terminal.id)} />
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -279,7 +502,11 @@ export default function Page() {
|
||||
<div class="relative h-full">
|
||||
<Tabs.Trigger
|
||||
value={props.tab}
|
||||
closeButton={<IconButton icon="close" variant="ghost" onClick={() => props.onTabClose(props.tab)} />}
|
||||
closeButton={
|
||||
<Tooltip value="Close tab" placement="bottom">
|
||||
<IconButton icon="close" variant="ghost" onClick={() => props.onTabClose(props.tab)} />
|
||||
</Tooltip>
|
||||
}
|
||||
hideCloseButton
|
||||
onClick={() => props.onTabClick(props.tab)}
|
||||
>
|
||||
@@ -322,7 +549,7 @@ export default function Page() {
|
||||
return typeof draggable.id === "string" ? draggable.id : undefined
|
||||
}
|
||||
|
||||
const wide = createMemo(() => layout.review.state() === "tab" || !session.diffs().length)
|
||||
const wide = createMemo(() => layout.review.state() === "tab" || !diffs().length)
|
||||
|
||||
return (
|
||||
<div class="relative bg-background-base size-full overflow-x-hidden flex flex-col">
|
||||
@@ -335,7 +562,7 @@ export default function Page() {
|
||||
>
|
||||
<DragDropSensors />
|
||||
<ConstrainDragYAxis />
|
||||
<Tabs value={session.layout.tabs.active ?? "chat"} onChange={session.layout.openTab}>
|
||||
<Tabs value={tabs().active() ?? "chat"} onChange={tabs().open}>
|
||||
<div class="sticky top-0 shrink-0 flex">
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="chat">
|
||||
@@ -345,41 +572,41 @@ export default function Page() {
|
||||
value={`${new Intl.NumberFormat("en-US", {
|
||||
notation: "compact",
|
||||
compactDisplay: "short",
|
||||
}).format(session.usage.tokens() ?? 0)} Tokens`}
|
||||
}).format(tokens() ?? 0)} Tokens`}
|
||||
class="flex items-center gap-1.5"
|
||||
>
|
||||
<ProgressCircle percentage={session.usage.context() ?? 0} />
|
||||
<div class="text-14-regular text-text-weak text-left w-7">{session.usage.context() ?? 0}%</div>
|
||||
<ProgressCircle percentage={context() ?? 0} />
|
||||
<div class="text-14-regular text-text-weak text-left w-7">{context() ?? 0}%</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
<Show when={layout.review.state() === "tab" && session.diffs().length}>
|
||||
<Show when={layout.review.state() === "tab" && diffs().length}>
|
||||
<Tabs.Trigger
|
||||
value="review"
|
||||
closeButton={
|
||||
<IconButton icon="collapse" size="normal" variant="ghost" onClick={layout.review.pane} />
|
||||
<Tooltip value="Close tab" placement="bottom">
|
||||
<IconButton icon="collapse" size="normal" variant="ghost" onClick={layout.review.pane} />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<Show when={session.diffs()}>
|
||||
<DiffChanges changes={session.diffs()} variant="bars" />
|
||||
<Show when={diffs()}>
|
||||
<DiffChanges changes={diffs()} variant="bars" />
|
||||
</Show>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<div>Review</div>
|
||||
<Show when={session.info()?.summary?.files}>
|
||||
<Show when={info()?.summary?.files}>
|
||||
<div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base">
|
||||
{session.info()?.summary?.files ?? 0}
|
||||
{info()?.summary?.files ?? 0}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
</Show>
|
||||
<SortableProvider ids={session.layout.tabs.all ?? []}>
|
||||
<For each={session.layout.tabs.all ?? []}>
|
||||
{(tab) => (
|
||||
<SortableTab tab={tab} onTabClick={handleTabClick} onTabClose={session.layout.closeTab} />
|
||||
)}
|
||||
<SortableProvider ids={tabs().all() ?? []}>
|
||||
<For each={tabs().all() ?? []}>
|
||||
{(tab) => <SortableTab tab={tab} onTabClick={handleTabClick} onTabClose={tabs().close} />}
|
||||
</For>
|
||||
</SortableProvider>
|
||||
<div class="bg-background-base h-full flex items-center justify-center border-b border-border-weak-base px-3">
|
||||
@@ -388,7 +615,7 @@ export default function Page() {
|
||||
icon="plus-small"
|
||||
variant="ghost"
|
||||
iconSize="large"
|
||||
onClick={() => setStore("fileSelectOpen", true)}
|
||||
onClick={() => dialog.show(() => <DialogSelectFile />)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -409,29 +636,33 @@ export default function Page() {
|
||||
}}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={session.id}>
|
||||
<Match when={params.id}>
|
||||
<div class="flex items-start justify-start h-full min-h-0">
|
||||
<SessionMessageRail
|
||||
messages={session.messages.user()}
|
||||
current={session.messages.active()}
|
||||
onMessageSelect={session.messages.setActive}
|
||||
messages={visibleUserMessages()}
|
||||
current={activeMessage()}
|
||||
onMessageSelect={setActiveMessage}
|
||||
wide={wide()}
|
||||
/>
|
||||
<SessionTurn
|
||||
sessionID={session.id!}
|
||||
messageID={session.messages.active()?.id!}
|
||||
classes={{
|
||||
root: "pb-20 flex-1 min-w-0",
|
||||
content: "pb-20",
|
||||
container:
|
||||
"w-full " +
|
||||
(wide()
|
||||
? "max-w-146 mx-auto px-6"
|
||||
: session.messages.user().length > 1
|
||||
? "pr-6 pl-18"
|
||||
: "px-6"),
|
||||
}}
|
||||
/>
|
||||
<Show when={activeMessage()}>
|
||||
<SessionTurn
|
||||
sessionID={params.id!}
|
||||
messageID={activeMessage()!.id}
|
||||
stepsExpanded={store.stepsExpanded}
|
||||
onStepsExpandedChange={(expanded) => setStore("stepsExpanded", expanded)}
|
||||
classes={{
|
||||
root: "pb-20 flex-1 min-w-0",
|
||||
content: "pb-20",
|
||||
container:
|
||||
"w-full " +
|
||||
(wide()
|
||||
? "max-w-146 mx-auto px-6"
|
||||
: visibleUserMessages().length > 1
|
||||
? "pr-6 pl-18"
|
||||
: "px-6"),
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
@@ -470,7 +701,7 @@ export default function Page() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={layout.review.state() === "pane" && session.diffs().length}>
|
||||
<Show when={layout.review.state() === "pane" && diffs().length}>
|
||||
<div
|
||||
classList={{
|
||||
"relative grow pt-3 flex-1 min-h-0 border-l border-border-weak-base": true,
|
||||
@@ -482,7 +713,7 @@ export default function Page() {
|
||||
header: "px-6",
|
||||
container: "px-6",
|
||||
}}
|
||||
diffs={session.diffs()}
|
||||
diffs={diffs()}
|
||||
actions={
|
||||
<Tooltip value="Open in tab">
|
||||
<IconButton
|
||||
@@ -490,7 +721,7 @@ export default function Page() {
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
layout.review.tab()
|
||||
session.layout.setActiveTab("review")
|
||||
tabs().setActive("review")
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -500,7 +731,7 @@ export default function Page() {
|
||||
</Show>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
<Show when={layout.review.state() === "tab" && session.diffs().length}>
|
||||
<Show when={layout.review.state() === "tab" && diffs().length}>
|
||||
<Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden">
|
||||
<div
|
||||
classList={{
|
||||
@@ -513,13 +744,13 @@ export default function Page() {
|
||||
header: "px-6",
|
||||
container: "px-6",
|
||||
}}
|
||||
diffs={session.diffs()}
|
||||
diffs={diffs()}
|
||||
split
|
||||
/>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Show>
|
||||
<For each={session.layout.tabs.all}>
|
||||
<For each={tabs().all()}>
|
||||
{(tab) => {
|
||||
const [file] = createResource(
|
||||
() => tab,
|
||||
@@ -535,7 +766,8 @@ export default function Page() {
|
||||
<Switch>
|
||||
<Match when={file()}>
|
||||
{(f) => (
|
||||
<Code
|
||||
<Dynamic
|
||||
component={codeComponent}
|
||||
file={{
|
||||
name: f().path,
|
||||
contents: f().content?.content ?? "",
|
||||
@@ -573,7 +805,7 @@ export default function Page() {
|
||||
</Show>
|
||||
</DragOverlay>
|
||||
</DragDropProvider>
|
||||
<Show when={session.layout.tabs.active}>
|
||||
<Show when={tabs().active()}>
|
||||
<div class="absolute inset-x-0 px-6 max-w-146 flex flex-col justify-center items-center z-50 mx-auto bottom-8">
|
||||
<PromptInput
|
||||
ref={(el) => {
|
||||
@@ -582,70 +814,6 @@ export default function Page() {
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<div class="hidden shrink-0 w-56 p-2 h-full overflow-y-auto">
|
||||
{/* <FileTree path="" onFileClick={ handleTabClick} /> */}
|
||||
</div>
|
||||
<div class="hidden shrink-0 w-56 p-2">
|
||||
<Show
|
||||
when={local.file.changes().length}
|
||||
fallback={<div class="px-2 text-xs text-text-muted">No changes</div>}
|
||||
>
|
||||
<ul class="">
|
||||
<For each={local.file.changes()}>
|
||||
{(path) => (
|
||||
<li>
|
||||
<button
|
||||
onClick={() => local.file.open(path, { view: "diff-unified", pinned: true })}
|
||||
class="w-full flex items-center px-2 py-0.5 gap-x-2 text-text-muted grow min-w-0 hover:bg-background-element"
|
||||
>
|
||||
<FileIcon node={{ path, type: "file" }} class="shrink-0 size-3" />
|
||||
<span class="text-xs text-text whitespace-nowrap">{getFilename(path)}</span>
|
||||
<span class="text-xs text-text-muted/60 whitespace-nowrap truncate min-w-0">
|
||||
{getDirectory(path)}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={store.fileSelectOpen}>
|
||||
<SelectDialog
|
||||
defaultOpen
|
||||
title="Select file"
|
||||
placeholder="Search files"
|
||||
emptyMessage="No files found"
|
||||
items={local.file.searchFiles}
|
||||
key={(x) => x}
|
||||
onOpenChange={(open) => setStore("fileSelectOpen", open)}
|
||||
onSelect={(x) => {
|
||||
if (x) {
|
||||
return session.layout.openTab("file://" + x)
|
||||
}
|
||||
return undefined
|
||||
}}
|
||||
>
|
||||
{(i) => (
|
||||
<div
|
||||
classList={{
|
||||
"w-full flex items-center justify-between rounded-md": true,
|
||||
}}
|
||||
>
|
||||
<div class="flex items-center gap-x-2 grow min-w-0">
|
||||
<FileIcon node={{ path: i, type: "file" }} class="shrink-0 size-4" />
|
||||
<div class="flex items-center text-14-regular">
|
||||
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
|
||||
{getDirectory(i)}
|
||||
</span>
|
||||
<span class="text-text-strong whitespace-nowrap">{getFilename(i)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-x-1 text-text-muted/40 shrink-0"></div>
|
||||
</div>
|
||||
)}
|
||||
</SelectDialog>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={layout.terminal.opened()}>
|
||||
<div
|
||||
@@ -669,25 +837,21 @@ export default function Page() {
|
||||
>
|
||||
<DragDropSensors />
|
||||
<ConstrainDragYAxis />
|
||||
<Tabs variant="alt" value={session.terminal.active()} onChange={session.terminal.open}>
|
||||
<Tabs variant="alt" value={terminal.active()} onChange={terminal.open}>
|
||||
<Tabs.List class="h-10">
|
||||
<SortableProvider ids={session.terminal.all().map((t) => t.id)}>
|
||||
<For each={session.terminal.all()}>{(terminal) => <SortableTerminalTab terminal={terminal} />}</For>
|
||||
<SortableProvider ids={terminal.all().map((t: LocalPTY) => t.id)}>
|
||||
<For each={terminal.all()}>{(pty) => <SortableTerminalTab terminal={pty} />}</For>
|
||||
</SortableProvider>
|
||||
<div class="h-full flex items-center justify-center">
|
||||
<Tooltip value="Open file" class="flex items-center">
|
||||
<IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={session.terminal.new} />
|
||||
<Tooltip value="New Terminal" class="flex items-center">
|
||||
<IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={terminal.new} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Tabs.List>
|
||||
<For each={session.terminal.all()}>
|
||||
{(terminal) => (
|
||||
<Tabs.Content value={terminal.id}>
|
||||
<Terminal
|
||||
pty={terminal}
|
||||
onCleanup={session.terminal.update}
|
||||
onConnectError={() => session.terminal.clone(terminal.id)}
|
||||
/>
|
||||
<For each={terminal.all()}>
|
||||
{(pty) => (
|
||||
<Tabs.Content value={pty.id}>
|
||||
<Terminal pty={pty} onCleanup={terminal.update} onConnectError={() => terminal.clone(pty.id)} />
|
||||
</Tabs.Content>
|
||||
)}
|
||||
</For>
|
||||
@@ -695,9 +859,9 @@ export default function Page() {
|
||||
<DragOverlay>
|
||||
<Show when={store.activeTerminalDraggable}>
|
||||
{(draggedId) => {
|
||||
const terminal = createMemo(() => session.terminal.all().find((t) => t.id === draggedId()))
|
||||
const pty = createMemo(() => terminal.all().find((t: LocalPTY) => t.id === draggedId()))
|
||||
return (
|
||||
<Show when={terminal()}>
|
||||
<Show when={pty()}>
|
||||
{(t) => (
|
||||
<div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular">
|
||||
{t().title}
|
||||
|
||||
47
packages/desktop/src/utils/prompt.ts
Normal file
47
packages/desktop/src/utils/prompt.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { Part, TextPart, FilePart } from "@opencode-ai/sdk/v2"
|
||||
import type { Prompt, FileAttachmentPart } from "@/context/prompt"
|
||||
|
||||
/**
|
||||
* Extract prompt content from message parts for restoring into the prompt input.
|
||||
* This is used by undo to restore the original user prompt.
|
||||
*/
|
||||
export function extractPromptFromParts(parts: Part[]): Prompt {
|
||||
const result: Prompt = []
|
||||
let position = 0
|
||||
|
||||
for (const part of parts) {
|
||||
if (part.type === "text") {
|
||||
const textPart = part as TextPart
|
||||
if (!textPart.synthetic && textPart.text) {
|
||||
result.push({
|
||||
type: "text",
|
||||
content: textPart.text,
|
||||
start: position,
|
||||
end: position + textPart.text.length,
|
||||
})
|
||||
position += textPart.text.length
|
||||
}
|
||||
} else if (part.type === "file") {
|
||||
const filePart = part as FilePart
|
||||
if (filePart.source?.type === "file") {
|
||||
const path = filePart.source.path
|
||||
const content = "@" + path
|
||||
const attachment: FileAttachmentPart = {
|
||||
type: "file",
|
||||
path,
|
||||
content,
|
||||
start: position,
|
||||
end: position + content.length,
|
||||
}
|
||||
result.push(attachment)
|
||||
position += content.length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.length === 0) {
|
||||
result.push({ type: "text", content: "", start: 0, end: 0 })
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.0.150",
|
||||
"version": "1.0.163",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -14,7 +14,7 @@
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"aws4fetch": "^1.0.20",
|
||||
"@pierre/precision-diffs": "catalog:",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solidjs/router": "catalog:",
|
||||
"@solidjs/start": "catalog:",
|
||||
"@solidjs/meta": "catalog:",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
||||
import { SessionReview } from "@opencode-ai/ui/session-review"
|
||||
import { DataProvider } from "@opencode-ai/ui/context"
|
||||
import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
|
||||
import { CodeComponentProvider } from "@opencode-ai/ui/context/code"
|
||||
import { createAsync, query, useParams } from "@solidjs/router"
|
||||
import { createEffect, createMemo, ErrorBoundary, For, Match, Show, Switch } from "solid-js"
|
||||
import { Share } from "~/core/share"
|
||||
@@ -19,7 +20,7 @@ import { createStore } from "solid-js/store"
|
||||
import z from "zod"
|
||||
import NotFound from "../[...404]"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { preloadMultiFileDiff, PreloadMultiFileDiffResult } from "@pierre/precision-diffs/ssr"
|
||||
import { preloadMultiFileDiff, PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
|
||||
import { Diff as SSRDiff } from "@opencode-ai/ui/diff-ssr"
|
||||
import { clientOnly } from "@solidjs/start"
|
||||
import { type IconName } from "@opencode-ai/ui/icons/provider"
|
||||
@@ -27,6 +28,7 @@ import { Meta } from "@solidjs/meta"
|
||||
import { Base64 } from "js-base64"
|
||||
|
||||
const ClientOnlyDiff = clientOnly(() => import("@opencode-ai/ui/diff").then((m) => ({ default: m.Diff })))
|
||||
const ClientOnlyCode = clientOnly(() => import("@opencode-ai/ui/code").then((m) => ({ default: m.Code })))
|
||||
|
||||
const SessionDataMissingError = NamedError.create(
|
||||
"SessionDataMissingError",
|
||||
@@ -138,18 +140,13 @@ const getData = query(async (shareID) => {
|
||||
|
||||
export default function () {
|
||||
const params = useParams()
|
||||
const data = createAsync(
|
||||
async () => {
|
||||
if (!params.shareID) throw new Error("Missing shareID")
|
||||
const now = Date.now()
|
||||
const data = getData(params.shareID)
|
||||
console.log("getData", Date.now() - now)
|
||||
return data
|
||||
},
|
||||
{
|
||||
deferStream: true,
|
||||
},
|
||||
)
|
||||
const data = createAsync(async () => {
|
||||
if (!params.shareID) throw new Error("Missing shareID")
|
||||
const now = Date.now()
|
||||
const data = getData(params.shareID)
|
||||
console.log("getData", Date.now() - now)
|
||||
return data
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
console.log(data())
|
||||
@@ -201,239 +198,254 @@ export default function () {
|
||||
<Meta property="og:image" content={ogImage()} />
|
||||
<Meta name="twitter:image" content={ogImage()} />
|
||||
<DiffComponentProvider component={ClientOnlyDiff}>
|
||||
<DataProvider data={data()} directory={info().directory}>
|
||||
{iife(() => {
|
||||
const [store, setStore] = createStore({
|
||||
messageId: undefined as string | undefined,
|
||||
})
|
||||
const messages = createMemo(() =>
|
||||
data().sessionID
|
||||
? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort(
|
||||
(a, b) => a.time.created - b.time.created,
|
||||
)
|
||||
: [],
|
||||
)
|
||||
const firstUserMessage = createMemo(() => messages().at(0))
|
||||
const activeMessage = createMemo(
|
||||
() => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(),
|
||||
)
|
||||
function setActiveMessage(message: UserMessage | undefined) {
|
||||
if (message) {
|
||||
setStore("messageId", message.id)
|
||||
} else {
|
||||
setStore("messageId", undefined)
|
||||
<CodeComponentProvider component={ClientOnlyCode}>
|
||||
<DataProvider data={data()} directory={info().directory}>
|
||||
{iife(() => {
|
||||
const [store, setStore] = createStore({
|
||||
messageId: undefined as string | undefined,
|
||||
})
|
||||
const messages = createMemo(() =>
|
||||
data().sessionID
|
||||
? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort(
|
||||
(a, b) => a.time.created - b.time.created,
|
||||
)
|
||||
: [],
|
||||
)
|
||||
const firstUserMessage = createMemo(() => messages().at(0))
|
||||
const activeMessage = createMemo(
|
||||
() => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(),
|
||||
)
|
||||
function setActiveMessage(message: UserMessage | undefined) {
|
||||
if (message) {
|
||||
setStore("messageId", message.id)
|
||||
} else {
|
||||
setStore("messageId", undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
const provider = createMemo(() => activeMessage()?.model?.providerID)
|
||||
const modelID = createMemo(() => activeMessage()?.model?.modelID)
|
||||
const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID()))
|
||||
const diffs = createMemo(() => {
|
||||
const diffs = data().session_diff[data().sessionID] ?? []
|
||||
const preloaded = data().session_diff_preload[data().sessionID] ?? []
|
||||
return diffs.map((diff) => ({
|
||||
...diff,
|
||||
preloaded: preloaded.find((d) => d.newFile.name === diff.file),
|
||||
}))
|
||||
})
|
||||
const splitDiffs = createMemo(() => {
|
||||
const diffs = data().session_diff[data().sessionID] ?? []
|
||||
const preloaded = data().session_diff_preload_split[data().sessionID] ?? []
|
||||
return diffs.map((diff) => ({
|
||||
...diff,
|
||||
preloaded: preloaded.find((d) => d.newFile.name === diff.file),
|
||||
}))
|
||||
})
|
||||
const provider = createMemo(() => activeMessage()?.model?.providerID)
|
||||
const modelID = createMemo(() => activeMessage()?.model?.modelID)
|
||||
const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID()))
|
||||
const diffs = createMemo(() => {
|
||||
const diffs = data().session_diff[data().sessionID] ?? []
|
||||
const preloaded = data().session_diff_preload[data().sessionID] ?? []
|
||||
return diffs.map((diff) => ({
|
||||
...diff,
|
||||
preloaded: preloaded.find((d) => d.newFile.name === diff.file),
|
||||
}))
|
||||
})
|
||||
const splitDiffs = createMemo(() => {
|
||||
const diffs = data().session_diff[data().sessionID] ?? []
|
||||
const preloaded = data().session_diff_preload_split[data().sessionID] ?? []
|
||||
return diffs.map((diff) => ({
|
||||
...diff,
|
||||
preloaded: preloaded.find((d) => d.newFile.name === diff.file),
|
||||
}))
|
||||
})
|
||||
|
||||
const title = () => (
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="h-8 flex gap-4 items-center justify-start self-stretch">
|
||||
<div class="pl-[2.5px] pr-2 flex items-center gap-1.75 bg-surface-strong shadow-xs-border-base">
|
||||
<Mark class="shrink-0 w-3 my-0.5" />
|
||||
<div class="text-12-mono text-text-base">v{info().version}</div>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<ProviderIcon id={provider() as IconName} class="size-3.5 shrink-0 text-icon-strong-base" />
|
||||
<div class="text-12-regular text-text-base">{model()?.name ?? modelID()}</div>
|
||||
</div>
|
||||
<div class="text-12-regular text-text-weaker">
|
||||
{DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-left text-16-medium text-text-strong">{info().title}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const turns = () => (
|
||||
<div class="relative mt-2 pt-6 pb-8 min-w-0 w-full h-full overflow-y-auto no-scrollbar">
|
||||
<div class="px-4">{title()}</div>
|
||||
<div class="flex flex-col gap-15 items-start justify-start mt-4">
|
||||
<For each={messages()}>
|
||||
{(message) => (
|
||||
<SessionTurn
|
||||
sessionID={data().sessionID}
|
||||
messageID={message.id}
|
||||
classes={{
|
||||
root: "min-w-0 w-full relative",
|
||||
content:
|
||||
"flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
|
||||
container: "px-4",
|
||||
}}
|
||||
const title = () => (
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="h-8 flex gap-4 items-center justify-start self-stretch">
|
||||
<div class="pl-[2.5px] pr-2 flex items-center gap-1.75 bg-surface-strong shadow-xs-border-base">
|
||||
<Mark class="shrink-0 w-3 my-0.5" />
|
||||
<div class="text-12-mono text-text-base">v{info().version}</div>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<ProviderIcon
|
||||
id={provider() as IconName}
|
||||
class="size-3.5 shrink-0 text-icon-strong-base"
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<div class="px-4 flex items-center justify-center pt-20 pb-8 shrink-0">
|
||||
<Logo class="w-58.5 opacity-12" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const wide = createMemo(() => diffs().length === 0)
|
||||
|
||||
return (
|
||||
<div class="relative bg-background-stronger w-screen h-screen overflow-hidden flex flex-col">
|
||||
<header class="h-12 px-6 py-2 flex items-center justify-between self-stretch bg-background-base border-b border-border-weak-base">
|
||||
<div class="">
|
||||
<a href="https://opencode.ai">
|
||||
<Mark />
|
||||
</a>
|
||||
<div class="text-12-regular text-text-base">{model()?.name ?? modelID()}</div>
|
||||
</div>
|
||||
<div class="text-12-regular text-text-weaker">
|
||||
{DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 items-center">
|
||||
<IconButton
|
||||
as={"a"}
|
||||
href="https://github.com/sst/opencode"
|
||||
target="_blank"
|
||||
icon="github"
|
||||
variant="ghost"
|
||||
/>
|
||||
<IconButton
|
||||
as={"a"}
|
||||
href="https://opencode.ai/discord"
|
||||
target="_blank"
|
||||
icon="discord"
|
||||
variant="ghost"
|
||||
/>
|
||||
<div class="text-left text-16-medium text-text-strong">{info().title}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const turns = () => (
|
||||
<div class="relative mt-2 pt-6 pb-8 min-w-0 w-full h-full overflow-y-auto no-scrollbar">
|
||||
<div class="px-4">{title()}</div>
|
||||
<div class="flex flex-col gap-15 items-start justify-start mt-4">
|
||||
<For each={messages()}>
|
||||
{(message) => (
|
||||
<SessionTurn
|
||||
sessionID={data().sessionID}
|
||||
messageID={message.id}
|
||||
classes={{
|
||||
root: "min-w-0 w-full relative",
|
||||
content:
|
||||
"flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
|
||||
container: "px-4",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</header>
|
||||
<div class="select-text flex flex-col flex-1 min-h-0">
|
||||
<div
|
||||
classList={{ "hidden w-full flex-1 min-h-0": true, "md:flex": wide(), "lg:flex": !wide() }}
|
||||
>
|
||||
<div class="px-4 flex items-center justify-center pt-20 pb-8 shrink-0">
|
||||
<Logo class="w-58.5 opacity-12" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const wide = createMemo(() => diffs().length === 0)
|
||||
|
||||
return (
|
||||
<div class="relative bg-background-stronger w-screen h-screen overflow-hidden flex flex-col">
|
||||
<header class="h-12 px-6 py-2 flex items-center justify-between self-stretch bg-background-base border-b border-border-weak-base">
|
||||
<div class="">
|
||||
<a href="https://opencode.ai">
|
||||
<Mark />
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex gap-3 items-center">
|
||||
<IconButton
|
||||
as={"a"}
|
||||
href="https://github.com/sst/opencode"
|
||||
target="_blank"
|
||||
icon="github"
|
||||
variant="ghost"
|
||||
/>
|
||||
<IconButton
|
||||
as={"a"}
|
||||
href="https://opencode.ai/discord"
|
||||
target="_blank"
|
||||
icon="discord"
|
||||
variant="ghost"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div class="select-text flex flex-col flex-1 min-h-0">
|
||||
<div
|
||||
classList={{
|
||||
"@container relative shrink-0 pt-14 flex flex-col gap-10 min-h-0 w-full": true,
|
||||
"mx-auto max-w-146": !wide(),
|
||||
"hidden w-full flex-1 min-h-0": true,
|
||||
"md:flex": wide(),
|
||||
"lg:flex": !wide(),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
"w-full flex justify-start items-start min-w-0": true,
|
||||
"max-w-146 mx-auto px-6": wide(),
|
||||
"pr-6 pl-18": !wide() && messages().length > 1,
|
||||
"px-6": !wide() && messages().length === 1,
|
||||
"@container relative shrink-0 pt-14 flex flex-col gap-10 min-h-0 w-full": true,
|
||||
"mx-auto max-w-146": !wide(),
|
||||
}}
|
||||
>
|
||||
{title()}
|
||||
</div>
|
||||
<div class="flex items-start justify-start h-full min-h-0">
|
||||
<SessionMessageRail
|
||||
messages={messages()}
|
||||
current={activeMessage()}
|
||||
onMessageSelect={setActiveMessage}
|
||||
wide={wide()}
|
||||
/>
|
||||
<SessionTurn
|
||||
sessionID={data().sessionID}
|
||||
messageID={store.messageId ?? firstUserMessage()!.id!}
|
||||
classes={{
|
||||
root: "grow",
|
||||
content: "flex flex-col justify-between items-start",
|
||||
container:
|
||||
"w-full pb-20 " +
|
||||
(wide()
|
||||
? "max-w-146 mx-auto px-6"
|
||||
: messages().length > 1
|
||||
? "pr-6 pl-18"
|
||||
: "px-6"),
|
||||
<div
|
||||
classList={{
|
||||
"w-full flex justify-start items-start min-w-0": true,
|
||||
"max-w-146 mx-auto px-6": wide(),
|
||||
"pr-6 pl-18": !wide() && messages().length > 1,
|
||||
"px-6": !wide() && messages().length === 1,
|
||||
}}
|
||||
>
|
||||
<div classList={{ "w-full flex items-center justify-center pb-8 shrink-0": true }}>
|
||||
<Logo class="w-58.5 opacity-12" />
|
||||
</div>
|
||||
</SessionTurn>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={diffs().length > 0}>
|
||||
<DiffComponentProvider component={SSRDiff}>
|
||||
<div class="@container relative grow pt-14 flex-1 min-h-0 border-l border-border-weak-base">
|
||||
<SessionReview
|
||||
class="@4xl:hidden"
|
||||
diffs={diffs()}
|
||||
classes={{
|
||||
root: "pb-20",
|
||||
header: "px-6",
|
||||
container: "px-6",
|
||||
}}
|
||||
/>
|
||||
<SessionReview
|
||||
split
|
||||
class="hidden @4xl:flex"
|
||||
diffs={splitDiffs()}
|
||||
classes={{
|
||||
root: "pb-20",
|
||||
header: "px-6",
|
||||
container: "px-6",
|
||||
}}
|
||||
/>
|
||||
{title()}
|
||||
</div>
|
||||
<div class="flex items-start justify-start h-full min-h-0">
|
||||
<SessionMessageRail
|
||||
messages={messages()}
|
||||
current={activeMessage()}
|
||||
onMessageSelect={setActiveMessage}
|
||||
wide={wide()}
|
||||
/>
|
||||
<SessionTurn
|
||||
sessionID={data().sessionID}
|
||||
messageID={store.messageId ?? firstUserMessage()!.id!}
|
||||
classes={{
|
||||
root: "grow",
|
||||
content: "flex flex-col justify-between items-start",
|
||||
container:
|
||||
"w-full pb-20 " +
|
||||
(wide()
|
||||
? "max-w-146 mx-auto px-6"
|
||||
: messages().length > 1
|
||||
? "pr-6 pl-18"
|
||||
: "px-6"),
|
||||
}}
|
||||
>
|
||||
<div classList={{ "w-full flex items-center justify-center pb-8 shrink-0": true }}>
|
||||
<Logo class="w-58.5 opacity-12" />
|
||||
</div>
|
||||
</SessionTurn>
|
||||
</div>
|
||||
</DiffComponentProvider>
|
||||
</Show>
|
||||
</div>
|
||||
<Switch>
|
||||
<Match when={diffs().length > 0}>
|
||||
<Tabs classList={{ "md:hidden": wide(), "lg:hidden": !wide() }}>
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="session" class="w-1/2" classes={{ button: "w-full" }}>
|
||||
Session
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="review" class="w-1/2 !border-r-0" classes={{ button: "w-full" }}>
|
||||
{diffs().length} Files Changed
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="session" class="!overflow-hidden">
|
||||
{turns()}
|
||||
</Tabs.Content>
|
||||
<Tabs.Content
|
||||
forceMount
|
||||
value="review"
|
||||
class="!overflow-hidden hidden data-[selected]:block"
|
||||
>
|
||||
<div class="relative h-full pt-8 overflow-y-auto no-scrollbar">
|
||||
<DiffComponentProvider component={SSRDiff}>
|
||||
<SessionReview
|
||||
diffs={diffs()}
|
||||
classes={{
|
||||
root: "pb-20",
|
||||
header: "px-4",
|
||||
container: "px-4",
|
||||
}}
|
||||
/>
|
||||
</DiffComponentProvider>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Tabs>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<div classList={{ "!overflow-hidden": true, "md:hidden": wide(), "lg:hidden": !wide() }}>
|
||||
{turns()}
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Show when={diffs().length > 0}>
|
||||
<DiffComponentProvider component={SSRDiff}>
|
||||
<div class="@container relative grow pt-14 flex-1 min-h-0 border-l border-border-weak-base">
|
||||
<SessionReview
|
||||
class="@4xl:hidden"
|
||||
diffs={diffs()}
|
||||
classes={{
|
||||
root: "pb-20",
|
||||
header: "px-6",
|
||||
container: "px-6",
|
||||
}}
|
||||
/>
|
||||
<SessionReview
|
||||
split
|
||||
class="hidden @4xl:flex"
|
||||
diffs={splitDiffs()}
|
||||
classes={{
|
||||
root: "pb-20",
|
||||
header: "px-6",
|
||||
container: "px-6",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DiffComponentProvider>
|
||||
</Show>
|
||||
</div>
|
||||
<Switch>
|
||||
<Match when={diffs().length > 0}>
|
||||
<Tabs classList={{ "md:hidden": wide(), "lg:hidden": !wide() }}>
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="session" class="w-1/2" classes={{ button: "w-full" }}>
|
||||
Session
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger
|
||||
value="review"
|
||||
class="w-1/2 !border-r-0"
|
||||
classes={{ button: "w-full" }}
|
||||
>
|
||||
{diffs().length} Files Changed
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="session" class="!overflow-hidden">
|
||||
{turns()}
|
||||
</Tabs.Content>
|
||||
<Tabs.Content
|
||||
forceMount
|
||||
value="review"
|
||||
class="!overflow-hidden hidden data-[selected]:block"
|
||||
>
|
||||
<div class="relative h-full pt-8 overflow-y-auto no-scrollbar">
|
||||
<DiffComponentProvider component={SSRDiff}>
|
||||
<SessionReview
|
||||
diffs={diffs()}
|
||||
classes={{
|
||||
root: "pb-20",
|
||||
header: "px-4",
|
||||
container: "px-4",
|
||||
}}
|
||||
/>
|
||||
</DiffComponentProvider>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Tabs>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<div
|
||||
classList={{ "!overflow-hidden": true, "md:hidden": wide(), "lg:hidden": !wide() }}
|
||||
>
|
||||
{turns()}
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</DataProvider>
|
||||
)
|
||||
})}
|
||||
</DataProvider>
|
||||
</CodeComponentProvider>
|
||||
</DiffComponentProvider>
|
||||
</>
|
||||
)
|
||||
|
||||
12
packages/enterprise/sst-env.d.ts
vendored
12
packages/enterprise/sst-env.d.ts
vendored
@@ -50,10 +50,6 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"Enterprise": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
"GITHUB_APP_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
@@ -94,6 +90,10 @@ declare module "sst" {
|
||||
"type": "sst.sst.Linkable"
|
||||
"value": string
|
||||
}
|
||||
"Teams": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
"Web": {
|
||||
"type": "sst.cloudflare.Astro"
|
||||
"url": string
|
||||
@@ -114,6 +114,10 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS5": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
}
|
||||
}
|
||||
// cloudflare
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.0.150"
|
||||
version = "1.0.163"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/sst/opencode"
|
||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.150/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.163/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.150/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.163/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.150/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.163/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.150/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.163/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.150/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.163/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.0.150",
|
||||
"version": "1.0.163",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -12,7 +12,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@octokit/rest": "catalog:",
|
||||
"hono": "catalog:",
|
||||
"jose": "6.0.11"
|
||||
}
|
||||
|
||||
12
packages/function/sst-env.d.ts
vendored
12
packages/function/sst-env.d.ts
vendored
@@ -50,10 +50,6 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"Enterprise": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
"GITHUB_APP_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
@@ -94,6 +90,10 @@ declare module "sst" {
|
||||
"type": "sst.sst.Linkable"
|
||||
"value": string
|
||||
}
|
||||
"Teams": {
|
||||
"type": "sst.cloudflare.SolidStart"
|
||||
"url": string
|
||||
}
|
||||
"Web": {
|
||||
"type": "sst.cloudflare.Astro"
|
||||
"url": string
|
||||
@@ -114,6 +114,10 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"ZEN_MODELS5": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
}
|
||||
}
|
||||
// cloudflare
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
FROM alpine
|
||||
FROM alpine AS base
|
||||
|
||||
# Disable the runtime transpiler cache by default inside Docker containers.
|
||||
# On ephemeral containers, the cache is not useful
|
||||
ARG BUN_RUNTIME_TRANSPILER_CACHE_PATH=0
|
||||
ENV BUN_RUNTIME_TRANSPILER_CACHE_PATH=${BUN_RUNTIME_TRANSPILER_CACHE_PATH}
|
||||
RUN apk add libgcc libstdc++ ripgrep
|
||||
ADD ./dist/opencode-linux-x64-baseline-musl/bin/opencode /usr/local/bin/opencode
|
||||
|
||||
FROM base AS build-amd64
|
||||
COPY dist/opencode-linux-x64-baseline-musl/bin/opencode /usr/local/bin/opencode
|
||||
|
||||
FROM base AS build-arm64
|
||||
COPY dist/opencode-linux-arm64-musl/bin/opencode /usr/local/bin/opencode
|
||||
|
||||
ARG TARGETARCH
|
||||
FROM build-${TARGETARCH}
|
||||
RUN opencode --version
|
||||
ENTRYPOINT ["opencode"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.0.150",
|
||||
"version": "1.0.163",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
@@ -64,7 +64,7 @@
|
||||
"@hono/zod-validator": "catalog:",
|
||||
"@modelcontextprotocol/sdk": "1.15.1",
|
||||
"@octokit/graphql": "9.0.2",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@octokit/rest": "catalog:",
|
||||
"@openauthjs/openauth": "catalog:",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
"@opencode-ai/script": "workspace:*",
|
||||
@@ -74,7 +74,7 @@
|
||||
"@opentui/core": "0.0.0-20251211-4403a69a",
|
||||
"@opentui/solid": "0.0.0-20251211-4403a69a",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@pierre/precision-diffs": "catalog:",
|
||||
"@pierre/diffs": "catalog:",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
"@standard-schema/spec": "1.0.0",
|
||||
"@zip.js/zip.js": "2.7.62",
|
||||
|
||||
@@ -117,6 +117,9 @@ for (const item of targets) {
|
||||
compile: {
|
||||
autoloadBunfig: false,
|
||||
autoloadDotenv: false,
|
||||
//@ts-ignore (bun types aren't up to date)
|
||||
autoloadTsconfig: true,
|
||||
autoloadPackageJson: true,
|
||||
target: name.replace(pkg.name, "bun") as any,
|
||||
outfile: `dist/${name}/bin/opencode`,
|
||||
execArgv: [`--user-agent=opencode/${Script.version}`, "--"],
|
||||
|
||||
@@ -244,8 +244,8 @@ if (!Script.preview) {
|
||||
await $`cd ./dist/homebrew-tap && git push`
|
||||
|
||||
const image = "ghcr.io/sst/opencode"
|
||||
await $`docker build -t ${image}:${Script.version} .`
|
||||
await $`docker push ${image}:${Script.version}`
|
||||
await $`docker tag ${image}:${Script.version} ${image}:latest`
|
||||
await $`docker push ${image}:latest`
|
||||
const platforms = "linux/amd64,linux/arm64"
|
||||
const tags = [`${image}:${Script.version}`, `${image}:latest`]
|
||||
const tagFlags = tags.flatMap((t) => ["-t", t])
|
||||
await $`docker buildx build --platform ${platforms} ${tagFlags} --push .`
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import { Config } from "@/config/config"
|
||||
import { Todo } from "@/session/todo"
|
||||
import { z } from "zod"
|
||||
import { LoadAPIKeyError } from "ai"
|
||||
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
|
||||
import type { OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2"
|
||||
|
||||
export namespace ACP {
|
||||
const log = Log.create({ service: "acp-agent" })
|
||||
@@ -386,7 +386,7 @@ export namespace ACP {
|
||||
|
||||
log.info("creating_session", { sessionId, mcpServers: params.mcpServers.length })
|
||||
|
||||
const load = await this.loadSession({
|
||||
const load = await this.loadSessionMode({
|
||||
cwd: directory,
|
||||
mcpServers: params.mcpServers,
|
||||
sessionId,
|
||||
@@ -412,6 +412,242 @@ export namespace ACP {
|
||||
}
|
||||
|
||||
async loadSession(params: LoadSessionRequest) {
|
||||
const directory = params.cwd
|
||||
const sessionId = params.sessionId
|
||||
|
||||
try {
|
||||
const model = await defaultModel(this.config, directory)
|
||||
|
||||
// Store ACP session state
|
||||
const state = await this.sessionManager.load(sessionId, params.cwd, params.mcpServers, model)
|
||||
|
||||
log.info("load_session", { sessionId, mcpServers: params.mcpServers.length })
|
||||
|
||||
const mode = await this.loadSessionMode({
|
||||
cwd: directory,
|
||||
mcpServers: params.mcpServers,
|
||||
sessionId,
|
||||
})
|
||||
|
||||
this.setupEventSubscriptions(state)
|
||||
|
||||
// Replay session history
|
||||
const messages = await this.sdk.session
|
||||
.messages(
|
||||
{
|
||||
sessionID: sessionId,
|
||||
directory,
|
||||
},
|
||||
{ throwOnError: true },
|
||||
)
|
||||
.then((x) => x.data)
|
||||
.catch((err) => {
|
||||
log.error("unexpected error when fetching message", { error: err })
|
||||
return undefined
|
||||
})
|
||||
|
||||
for (const msg of messages ?? []) {
|
||||
log.debug("replay message", msg)
|
||||
await this.processMessage(msg)
|
||||
}
|
||||
|
||||
return mode
|
||||
} catch (e) {
|
||||
const error = MessageV2.fromError(e, {
|
||||
providerID: this.config.defaultModel?.providerID ?? "unknown",
|
||||
})
|
||||
if (LoadAPIKeyError.isInstance(error)) {
|
||||
throw RequestError.authRequired()
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private async processMessage(message: SessionMessageResponse) {
|
||||
log.debug("process message", message)
|
||||
if (message.info.role !== "assistant" && message.info.role !== "user") return
|
||||
const sessionId = message.info.sessionID
|
||||
|
||||
for (const part of message.parts) {
|
||||
if (part.type === "tool") {
|
||||
switch (part.state.status) {
|
||||
case "pending":
|
||||
await this.connection
|
||||
.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: "tool_call",
|
||||
toolCallId: part.callID,
|
||||
title: part.tool,
|
||||
kind: toToolKind(part.tool),
|
||||
status: "pending",
|
||||
locations: [],
|
||||
rawInput: {},
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error("failed to send tool pending to ACP", { error: err })
|
||||
})
|
||||
break
|
||||
case "running":
|
||||
await this.connection
|
||||
.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: part.callID,
|
||||
status: "in_progress",
|
||||
locations: toLocations(part.tool, part.state.input),
|
||||
rawInput: part.state.input,
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error("failed to send tool in_progress to ACP", { error: err })
|
||||
})
|
||||
break
|
||||
case "completed":
|
||||
const kind = toToolKind(part.tool)
|
||||
const content: ToolCallContent[] = [
|
||||
{
|
||||
type: "content",
|
||||
content: {
|
||||
type: "text",
|
||||
text: part.state.output,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
if (kind === "edit") {
|
||||
const input = part.state.input
|
||||
const filePath = typeof input["filePath"] === "string" ? input["filePath"] : ""
|
||||
const oldText = typeof input["oldString"] === "string" ? input["oldString"] : ""
|
||||
const newText =
|
||||
typeof input["newString"] === "string"
|
||||
? input["newString"]
|
||||
: typeof input["content"] === "string"
|
||||
? input["content"]
|
||||
: ""
|
||||
content.push({
|
||||
type: "diff",
|
||||
path: filePath,
|
||||
oldText,
|
||||
newText,
|
||||
})
|
||||
}
|
||||
|
||||
if (part.tool === "todowrite") {
|
||||
const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output))
|
||||
if (parsedTodos.success) {
|
||||
await this.connection
|
||||
.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: "plan",
|
||||
entries: parsedTodos.data.map((todo) => {
|
||||
const status: PlanEntry["status"] =
|
||||
todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"])
|
||||
return {
|
||||
priority: "medium",
|
||||
status,
|
||||
content: todo.content,
|
||||
}
|
||||
}),
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error("failed to send session update for todo", { error: err })
|
||||
})
|
||||
} else {
|
||||
log.error("failed to parse todo output", { error: parsedTodos.error })
|
||||
}
|
||||
}
|
||||
|
||||
await this.connection
|
||||
.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: part.callID,
|
||||
status: "completed",
|
||||
kind,
|
||||
content,
|
||||
title: part.state.title,
|
||||
rawOutput: {
|
||||
output: part.state.output,
|
||||
metadata: part.state.metadata,
|
||||
},
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error("failed to send tool completed to ACP", { error: err })
|
||||
})
|
||||
break
|
||||
case "error":
|
||||
await this.connection
|
||||
.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: "tool_call_update",
|
||||
toolCallId: part.callID,
|
||||
status: "failed",
|
||||
content: [
|
||||
{
|
||||
type: "content",
|
||||
content: {
|
||||
type: "text",
|
||||
text: part.state.error,
|
||||
},
|
||||
},
|
||||
],
|
||||
rawOutput: {
|
||||
error: part.state.error,
|
||||
},
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error("failed to send tool error to ACP", { error: err })
|
||||
})
|
||||
break
|
||||
}
|
||||
} else if (part.type === "text") {
|
||||
if (part.text) {
|
||||
await this.connection
|
||||
.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: message.info.role === "user" ? "user_message_chunk" : "agent_message_chunk",
|
||||
content: {
|
||||
type: "text",
|
||||
text: part.text,
|
||||
},
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error("failed to send text to ACP", { error: err })
|
||||
})
|
||||
}
|
||||
} else if (part.type === "reasoning") {
|
||||
if (part.text) {
|
||||
await this.connection
|
||||
.sessionUpdate({
|
||||
sessionId,
|
||||
update: {
|
||||
sessionUpdate: "agent_thought_chunk",
|
||||
content: {
|
||||
type: "text",
|
||||
text: part.text,
|
||||
},
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error("failed to send reasoning to ACP", { error: err })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async loadSessionMode(params: LoadSessionRequest) {
|
||||
const directory = params.cwd
|
||||
const model = await defaultModel(this.config, directory)
|
||||
const sessionId = params.sessionId
|
||||
@@ -678,6 +914,8 @@ export namespace ACP {
|
||||
{
|
||||
sessionID,
|
||||
directory,
|
||||
providerID: model.providerID,
|
||||
modelID: model.modelID,
|
||||
},
|
||||
{ throwOnError: true },
|
||||
)
|
||||
|
||||
@@ -40,6 +40,37 @@ export class ACPSessionManager {
|
||||
return state
|
||||
}
|
||||
|
||||
async load(
|
||||
sessionId: string,
|
||||
cwd: string,
|
||||
mcpServers: McpServer[],
|
||||
model?: ACPSessionState["model"],
|
||||
): Promise<ACPSessionState> {
|
||||
const session = await this.sdk.session
|
||||
.get(
|
||||
{
|
||||
sessionID: sessionId,
|
||||
directory: cwd,
|
||||
},
|
||||
{ throwOnError: true },
|
||||
)
|
||||
.then((x) => x.data!)
|
||||
|
||||
const resolvedModel = model
|
||||
|
||||
const state: ACPSessionState = {
|
||||
id: sessionId,
|
||||
cwd,
|
||||
mcpServers,
|
||||
createdAt: new Date(session.time.created),
|
||||
model: resolvedModel,
|
||||
}
|
||||
log.info("loading_session", { state })
|
||||
|
||||
this.sessions.set(sessionId, state)
|
||||
return state
|
||||
}
|
||||
|
||||
get(sessionId: string): ACPSessionState {
|
||||
const session = this.sessions.get(sessionId)
|
||||
if (!session) {
|
||||
|
||||
@@ -2,18 +2,24 @@ import { Config } from "../config/config"
|
||||
import z from "zod"
|
||||
import { Provider } from "../provider/provider"
|
||||
import { generateObject, type ModelMessage } from "ai"
|
||||
import PROMPT_GENERATE from "./generate.txt"
|
||||
import { SystemPrompt } from "../session/system"
|
||||
import { Instance } from "../project/instance"
|
||||
import { mergeDeep } from "remeda"
|
||||
|
||||
import PROMPT_GENERATE from "./generate.txt"
|
||||
import PROMPT_COMPACTION from "./prompt/compaction.txt"
|
||||
import PROMPT_EXPLORE from "./prompt/explore.txt"
|
||||
import PROMPT_SUMMARY from "./prompt/summary.txt"
|
||||
import PROMPT_TITLE from "./prompt/title.txt"
|
||||
|
||||
export namespace Agent {
|
||||
export const Info = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
mode: z.enum(["subagent", "primary", "all"]),
|
||||
builtIn: z.boolean(),
|
||||
native: z.boolean().optional(),
|
||||
hidden: z.boolean().optional(),
|
||||
topP: z.number().optional(),
|
||||
temperature: z.number().optional(),
|
||||
color: z.string().optional(),
|
||||
@@ -101,6 +107,24 @@ export namespace Agent {
|
||||
)
|
||||
|
||||
const result: Record<string, Info> = {
|
||||
build: {
|
||||
name: "build",
|
||||
tools: { ...defaultTools },
|
||||
options: {},
|
||||
permission: agentPermission,
|
||||
mode: "primary",
|
||||
native: true,
|
||||
},
|
||||
plan: {
|
||||
name: "plan",
|
||||
options: {},
|
||||
permission: planPermission,
|
||||
tools: {
|
||||
...defaultTools,
|
||||
},
|
||||
mode: "primary",
|
||||
native: true,
|
||||
},
|
||||
general: {
|
||||
name: "general",
|
||||
description: `General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel.`,
|
||||
@@ -112,7 +136,8 @@ export namespace Agent {
|
||||
options: {},
|
||||
permission: agentPermission,
|
||||
mode: "subagent",
|
||||
builtIn: true,
|
||||
native: true,
|
||||
hidden: true,
|
||||
},
|
||||
explore: {
|
||||
name: "explore",
|
||||
@@ -124,48 +149,43 @@ export namespace Agent {
|
||||
...defaultTools,
|
||||
},
|
||||
description: `Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.`,
|
||||
prompt: [
|
||||
`You are a file search specialist. You excel at thoroughly navigating and exploring codebases.`,
|
||||
``,
|
||||
`Your strengths:`,
|
||||
`- Rapidly finding files using glob patterns`,
|
||||
`- Searching code and text with powerful regex patterns`,
|
||||
`- Reading and analyzing file contents`,
|
||||
``,
|
||||
`Guidelines:`,
|
||||
`- Use Glob for broad file pattern matching`,
|
||||
`- Use Grep for searching file contents with regex`,
|
||||
`- Use Read when you know the specific file path you need to read`,
|
||||
`- Use Bash for file operations like copying, moving, or listing directory contents`,
|
||||
`- Adapt your search approach based on the thoroughness level specified by the caller`,
|
||||
`- Return file paths as absolute paths in your final response`,
|
||||
`- For clear communication, avoid using emojis`,
|
||||
`- Do not create any files, or run bash commands that modify the user's system state in any way`,
|
||||
``,
|
||||
`Complete the user's search request efficiently and report your findings clearly.`,
|
||||
].join("\n"),
|
||||
prompt: PROMPT_EXPLORE,
|
||||
options: {},
|
||||
permission: agentPermission,
|
||||
mode: "subagent",
|
||||
builtIn: true,
|
||||
native: true,
|
||||
},
|
||||
build: {
|
||||
name: "build",
|
||||
tools: { ...defaultTools },
|
||||
compaction: {
|
||||
name: "compaction",
|
||||
mode: "primary",
|
||||
native: true,
|
||||
hidden: true,
|
||||
prompt: PROMPT_COMPACTION,
|
||||
tools: {
|
||||
"*": false,
|
||||
},
|
||||
options: {},
|
||||
permission: agentPermission,
|
||||
mode: "primary",
|
||||
builtIn: true,
|
||||
},
|
||||
plan: {
|
||||
name: "plan",
|
||||
options: {},
|
||||
permission: planPermission,
|
||||
tools: {
|
||||
...defaultTools,
|
||||
},
|
||||
title: {
|
||||
name: "title",
|
||||
mode: "primary",
|
||||
builtIn: true,
|
||||
options: {},
|
||||
native: true,
|
||||
hidden: true,
|
||||
permission: agentPermission,
|
||||
prompt: PROMPT_TITLE,
|
||||
tools: {},
|
||||
},
|
||||
summary: {
|
||||
name: "summary",
|
||||
mode: "primary",
|
||||
options: {},
|
||||
native: true,
|
||||
hidden: true,
|
||||
permission: agentPermission,
|
||||
prompt: PROMPT_SUMMARY,
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
for (const [key, value] of Object.entries(cfg.agent ?? {})) {
|
||||
@@ -181,7 +201,7 @@ export namespace Agent {
|
||||
permission: agentPermission,
|
||||
options: {},
|
||||
tools: {},
|
||||
builtIn: false,
|
||||
native: false,
|
||||
}
|
||||
const {
|
||||
name,
|
||||
@@ -236,9 +256,9 @@ export namespace Agent {
|
||||
return state().then((x) => Object.values(x))
|
||||
}
|
||||
|
||||
export async function generate(input: { description: string }) {
|
||||
export async function generate(input: { description: string; model?: { providerID: string; modelID: string } }) {
|
||||
const cfg = await Config.get()
|
||||
const defaultModel = await Provider.defaultModel()
|
||||
const defaultModel = input.model ?? (await Provider.defaultModel())
|
||||
const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID)
|
||||
const language = await Provider.getLanguage(model)
|
||||
const system = SystemPrompt.header(defaultModel.providerID)
|
||||
|
||||
18
packages/opencode/src/agent/prompt/explore.txt
Normal file
18
packages/opencode/src/agent/prompt/explore.txt
Normal file
@@ -0,0 +1,18 @@
|
||||
You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
|
||||
|
||||
Your strengths:
|
||||
- Rapidly finding files using glob patterns
|
||||
- Searching code and text with powerful regex patterns
|
||||
- Reading and analyzing file contents
|
||||
|
||||
Guidelines:
|
||||
- Use Glob for broad file pattern matching
|
||||
- Use Grep for searching file contents with regex
|
||||
- Use Read when you know the specific file path you need to read
|
||||
- Use Bash for file operations like copying, moving, or listing directory contents
|
||||
- Adapt your search approach based on the thoroughness level specified by the caller
|
||||
- Return file paths as absolute paths in your final response
|
||||
- For clear communication, avoid using emojis
|
||||
- Do not create any files, or run bash commands that modify the user's system state in any way
|
||||
|
||||
Complete the user's search request efficiently and report your findings clearly.
|
||||
@@ -22,8 +22,8 @@ Your output must be:
|
||||
- The title should NEVER include "summarizing" or "generating" when generating a title
|
||||
- DO NOT SAY YOU CANNOT GENERATE A TITLE OR COMPLAIN ABOUT THE INPUT
|
||||
- Always output something meaningful, even if the input is minimal.
|
||||
- If the user message is short or conversational (e.g. “hello”, “lol”, “whats up”, “hey”):
|
||||
→ create a title that reflects the user’s tone or intent (such as Greeting, Quick check-in, Light chat, Intro message, etc.)
|
||||
- If the user message is short or conversational (e.g. "hello", "lol", "whats up", "hey"):
|
||||
→ create a title that reflects the user's tone or intent (such as Greeting, Quick check-in, Light chat, Intro message, etc.)
|
||||
</rules>
|
||||
|
||||
<examples>
|
||||
@@ -85,47 +85,16 @@ export namespace BunProc {
|
||||
version,
|
||||
})
|
||||
|
||||
const total = 3
|
||||
const wait = 500
|
||||
|
||||
const runInstall = async (count: number = 1): Promise<void> => {
|
||||
log.info("bun install attempt", {
|
||||
pkg,
|
||||
version,
|
||||
attempt: count,
|
||||
total,
|
||||
})
|
||||
await BunProc.run(args, {
|
||||
cwd: Global.Path.cache,
|
||||
}).catch(async (error) => {
|
||||
log.warn("bun install failed", {
|
||||
pkg,
|
||||
version,
|
||||
attempt: count,
|
||||
total,
|
||||
error,
|
||||
})
|
||||
if (count >= total) {
|
||||
throw new InstallFailedError(
|
||||
{ pkg, version },
|
||||
{
|
||||
cause: error,
|
||||
},
|
||||
)
|
||||
}
|
||||
const delay = wait * count
|
||||
log.info("bun install retrying", {
|
||||
pkg,
|
||||
version,
|
||||
next: count + 1,
|
||||
delay,
|
||||
})
|
||||
await Bun.sleep(delay)
|
||||
return runInstall(count + 1)
|
||||
})
|
||||
}
|
||||
|
||||
await runInstall()
|
||||
await BunProc.run(args, {
|
||||
cwd: Global.Path.cache,
|
||||
}).catch((e) => {
|
||||
throw new InstallFailedError(
|
||||
{ pkg, version },
|
||||
{
|
||||
cause: e,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
// Resolve actual version from installed package when using "latest"
|
||||
// This ensures subsequent starts use the cached version until explicitly updated
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as prompts from "@clack/prompts"
|
||||
import { UI } from "../ui"
|
||||
import { Global } from "../../global"
|
||||
import { Agent } from "../../agent/agent"
|
||||
import { Provider } from "../../provider/provider"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import matter from "gray-matter"
|
||||
@@ -47,6 +48,11 @@ const AgentCreateCommand = cmd({
|
||||
.option("tools", {
|
||||
type: "string",
|
||||
describe: `comma-separated list of tools to enable (default: all). Available: "${AVAILABLE_TOOLS.join(", ")}"`,
|
||||
})
|
||||
.option("model", {
|
||||
type: "string",
|
||||
alias: ["m"],
|
||||
describe: "model to use in the format of provider/model",
|
||||
}),
|
||||
async handler(args) {
|
||||
await Instance.provide({
|
||||
@@ -114,7 +120,8 @@ const AgentCreateCommand = cmd({
|
||||
// Generate agent
|
||||
const spinner = prompts.spinner()
|
||||
spinner.start("Generating agent configuration...")
|
||||
const generated = await Agent.generate({ description }).catch((error) => {
|
||||
const model = args.model ? Provider.parseModel(args.model) : undefined
|
||||
const generated = await Agent.generate({ description, model }).catch((error) => {
|
||||
spinner.stop(`LLM failed to generate agent: ${error.message}`, 1)
|
||||
if (isFullyNonInteractive) process.exit(1)
|
||||
throw new UI.CancelledError()
|
||||
@@ -227,8 +234,8 @@ const AgentListCommand = cmd({
|
||||
async fn() {
|
||||
const agents = await Agent.list()
|
||||
const sortedAgents = agents.sort((a, b) => {
|
||||
if (a.builtIn !== b.builtIn) {
|
||||
return a.builtIn ? -1 : 1
|
||||
if (a.native !== b.native) {
|
||||
return a.native ? -1 : 1
|
||||
}
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
|
||||
@@ -17,6 +17,7 @@ const DiagnosticsCommand = cmd({
|
||||
async handler(args) {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
await LSP.touchFile(args.file, true)
|
||||
await Bun.sleep(1000)
|
||||
process.stdout.write(JSON.stringify(await LSP.diagnostics(), null, 2) + EOL)
|
||||
})
|
||||
},
|
||||
|
||||
@@ -128,6 +128,19 @@ const AGENT_USERNAME = "opencode-agent[bot]"
|
||||
const AGENT_REACTION = "eyes"
|
||||
const WORKFLOW_FILE = ".github/workflows/opencode.yml"
|
||||
|
||||
// Parses GitHub remote URLs in various formats:
|
||||
// - https://github.com/owner/repo.git
|
||||
// - https://github.com/owner/repo
|
||||
// - git@github.com:owner/repo.git
|
||||
// - git@github.com:owner/repo
|
||||
// - ssh://git@github.com/owner/repo.git
|
||||
// - ssh://git@github.com/owner/repo
|
||||
export function parseGitHubRemote(url: string): { owner: string; repo: string } | null {
|
||||
const match = url.match(/^(?:(?:https?|ssh):\/\/)?(?:git@)?github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/)
|
||||
if (!match) return null
|
||||
return { owner: match[1], repo: match[2] }
|
||||
}
|
||||
|
||||
export const GithubCommand = cmd({
|
||||
command: "github",
|
||||
describe: "manage GitHub agent",
|
||||
@@ -197,20 +210,12 @@ export const GithubInstallCommand = cmd({
|
||||
|
||||
// Get repo info
|
||||
const info = (await $`git remote get-url origin`.quiet().nothrow().text()).trim()
|
||||
// match https or git pattern
|
||||
// ie. https://github.com/sst/opencode.git
|
||||
// ie. https://github.com/sst/opencode
|
||||
// ie. git@github.com:sst/opencode.git
|
||||
// ie. git@github.com:sst/opencode
|
||||
// ie. ssh://git@github.com/sst/opencode.git
|
||||
// ie. ssh://git@github.com/sst/opencode
|
||||
const parsed = info.match(/^(?:(?:https?|ssh):\/\/)?(?:git@)?github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/)
|
||||
const parsed = parseGitHubRemote(info)
|
||||
if (!parsed) {
|
||||
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
|
||||
throw new UI.CancelledError()
|
||||
}
|
||||
const [, owner, repo] = parsed
|
||||
return { owner, repo, root: Instance.worktree }
|
||||
return { owner: parsed.owner, repo: parsed.repo, root: Instance.worktree }
|
||||
}
|
||||
|
||||
async function promptProvider() {
|
||||
@@ -278,7 +283,7 @@ export const GithubInstallCommand = cmd({
|
||||
process.platform === "darwin"
|
||||
? `open "${url}"`
|
||||
: process.platform === "win32"
|
||||
? `start "${url}"`
|
||||
? `start "" "${url}"`
|
||||
: `xdg-open "${url}"`
|
||||
|
||||
exec(command, (error) => {
|
||||
@@ -411,17 +416,30 @@ export const GithubRunCommand = cmd({
|
||||
let exitCode = 0
|
||||
type PromptFiles = Awaited<ReturnType<typeof getUserPrompt>>["promptFiles"]
|
||||
const triggerCommentId = payload.comment.id
|
||||
const useGithubToken = normalizeUseGithubToken()
|
||||
|
||||
try {
|
||||
const actionToken = isMock ? args.token! : await getOidcToken()
|
||||
appToken = await exchangeForAppToken(actionToken)
|
||||
if (useGithubToken) {
|
||||
const githubToken = process.env["GITHUB_TOKEN"]
|
||||
if (!githubToken) {
|
||||
throw new Error(
|
||||
"GITHUB_TOKEN environment variable is not set. When using use_github_token, you must provide GITHUB_TOKEN.",
|
||||
)
|
||||
}
|
||||
appToken = githubToken
|
||||
} else {
|
||||
const actionToken = isMock ? args.token! : await getOidcToken()
|
||||
appToken = await exchangeForAppToken(actionToken)
|
||||
}
|
||||
octoRest = new Octokit({ auth: appToken })
|
||||
octoGraph = graphql.defaults({
|
||||
headers: { authorization: `token ${appToken}` },
|
||||
})
|
||||
|
||||
const { userPrompt, promptFiles } = await getUserPrompt()
|
||||
await configureGit(appToken)
|
||||
if (!useGithubToken) {
|
||||
await configureGit(appToken)
|
||||
}
|
||||
await assertPermissions()
|
||||
|
||||
await addReaction()
|
||||
@@ -514,8 +532,10 @@ export const GithubRunCommand = cmd({
|
||||
// Also output the clean error message for the action to capture
|
||||
//core.setOutput("prepare_error", e.message);
|
||||
} finally {
|
||||
await restoreGitConfig()
|
||||
await revokeAppToken()
|
||||
if (!useGithubToken) {
|
||||
await restoreGitConfig()
|
||||
await revokeAppToken()
|
||||
}
|
||||
}
|
||||
process.exit(exitCode)
|
||||
|
||||
@@ -544,6 +564,14 @@ export const GithubRunCommand = cmd({
|
||||
throw new Error(`Invalid share value: ${value}. Share must be a boolean.`)
|
||||
}
|
||||
|
||||
function normalizeUseGithubToken() {
|
||||
const value = process.env["USE_GITHUB_TOKEN"]
|
||||
if (!value) return false
|
||||
if (value === "true") return true
|
||||
if (value === "false") return false
|
||||
throw new Error(`Invalid use_github_token value: ${value}. Must be a boolean.`)
|
||||
}
|
||||
|
||||
function isIssueCommentEvent(
|
||||
event: IssueCommentEvent | PullRequestReviewCommentEvent,
|
||||
): event is IssueCommentEvent {
|
||||
|
||||
@@ -88,7 +88,9 @@ export const RunCommand = cmd({
|
||||
})
|
||||
},
|
||||
handler: async (args) => {
|
||||
let message = [...args.message, ...(args["--"] || [])].join(" ")
|
||||
let message = [...args.message, ...(args["--"] || [])]
|
||||
.map((arg) => (arg.includes(" ") ? `"${arg.replace(/"/g, '\\"')}"` : arg))
|
||||
.join(" ")
|
||||
|
||||
const fileParts: any[] = []
|
||||
if (args.file) {
|
||||
@@ -277,8 +279,8 @@ export const RunCommand = cmd({
|
||||
}
|
||||
return { error }
|
||||
})
|
||||
if (!shareResult.error) {
|
||||
UI.println(UI.Style.TEXT_INFO_BOLD + "~ https://opencode.ai/s/" + sessionID.slice(-8))
|
||||
if (!shareResult.error && "data" in shareResult && shareResult.data?.share?.url) {
|
||||
UI.println(UI.Style.TEXT_INFO_BOLD + "~ " + shareResult.data.share.url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,8 +332,8 @@ export const RunCommand = cmd({
|
||||
}
|
||||
return { error }
|
||||
})
|
||||
if (!shareResult.error) {
|
||||
UI.println(UI.Style.TEXT_INFO_BOLD + "~ https://opencode.ai/s/" + sessionID.slice(-8))
|
||||
if (!shareResult.error && "data" in shareResult && shareResult.data?.share?.url) {
|
||||
UI.println(UI.Style.TEXT_INFO_BOLD + "~ " + shareResult.data.share.url)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -107,7 +107,9 @@ export function tui(input: { url: string; args: Args; onExit?: () => Promise<voi
|
||||
render(
|
||||
() => {
|
||||
return (
|
||||
<ErrorBoundary fallback={(error, reset) => <ErrorComponent error={error} reset={reset} onExit={onExit} />}>
|
||||
<ErrorBoundary
|
||||
fallback={(error, reset) => <ErrorComponent error={error} reset={reset} onExit={onExit} mode={mode} />}
|
||||
>
|
||||
<ArgsProvider {...input.args}>
|
||||
<ExitProvider onExit={onExit}>
|
||||
<KVProvider>
|
||||
@@ -216,7 +218,9 @@ function App() {
|
||||
let continued = false
|
||||
createEffect(() => {
|
||||
if (continued || sync.status !== "complete" || !args.continue) return
|
||||
const match = sync.data.session.at(0)?.id
|
||||
const match = sync.data.session
|
||||
.toSorted((a, b) => b.time.updated - a.time.updated)
|
||||
.find((x) => x.parentID === undefined)?.id
|
||||
if (match) {
|
||||
continued = true
|
||||
route.navigate({ type: "session", sessionID: match })
|
||||
@@ -293,6 +297,24 @@ function App() {
|
||||
local.model.cycle(-1)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Favorite cycle",
|
||||
value: "model.cycle_favorite",
|
||||
keybind: "model_cycle_favorite",
|
||||
category: "Agent",
|
||||
onSelect: () => {
|
||||
local.model.cycleFavorite(1)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Favorite cycle reverse",
|
||||
value: "model.cycle_favorite_reverse",
|
||||
keybind: "model_cycle_favorite_reverse",
|
||||
category: "Agent",
|
||||
onSelect: () => {
|
||||
local.model.cycleFavorite(-1)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Switch agent",
|
||||
value: "agent.list",
|
||||
@@ -536,7 +558,12 @@ function App() {
|
||||
)
|
||||
}
|
||||
|
||||
function ErrorComponent(props: { error: Error; reset: () => void; onExit: () => Promise<void> }) {
|
||||
function ErrorComponent(props: {
|
||||
error: Error
|
||||
reset: () => void
|
||||
onExit: () => Promise<void>
|
||||
mode?: "dark" | "light"
|
||||
}) {
|
||||
const term = useTerminalDimensions()
|
||||
useKeyboard((evt) => {
|
||||
if (evt.ctrl && evt.name === "c") {
|
||||
@@ -547,6 +574,15 @@ function ErrorComponent(props: { error: Error; reset: () => void; onExit: () =>
|
||||
|
||||
const issueURL = new URL("https://github.com/sst/opencode/issues/new?template=bug-report.yml")
|
||||
|
||||
// Choose safe fallback colors per mode since theme context may not be available
|
||||
const isLight = props.mode === "light"
|
||||
const colors = {
|
||||
bg: isLight ? "#ffffff" : "#0a0a0a",
|
||||
text: isLight ? "#1a1a1a" : "#eeeeee",
|
||||
muted: isLight ? "#8a8a8a" : "#808080",
|
||||
primary: isLight ? "#3b7dd8" : "#fab283",
|
||||
}
|
||||
|
||||
if (props.error.message) {
|
||||
issueURL.searchParams.set("title", `opentui: fatal: ${props.error.message}`)
|
||||
}
|
||||
@@ -567,27 +603,31 @@ function ErrorComponent(props: { error: Error; reset: () => void; onExit: () =>
|
||||
}
|
||||
|
||||
return (
|
||||
<box flexDirection="column" gap={1}>
|
||||
<box flexDirection="column" gap={1} backgroundColor={colors.bg}>
|
||||
<box flexDirection="row" gap={1} alignItems="center">
|
||||
<text attributes={TextAttributes.BOLD}>Please report an issue.</text>
|
||||
<box onMouseUp={copyIssueURL} backgroundColor="#565f89" padding={1}>
|
||||
<text attributes={TextAttributes.BOLD}>Copy issue URL (exception info pre-filled)</text>
|
||||
<text attributes={TextAttributes.BOLD} fg={colors.text}>
|
||||
Please report an issue.
|
||||
</text>
|
||||
<box onMouseUp={copyIssueURL} backgroundColor={colors.primary} padding={1}>
|
||||
<text attributes={TextAttributes.BOLD} fg={colors.bg}>
|
||||
Copy issue URL (exception info pre-filled)
|
||||
</text>
|
||||
</box>
|
||||
{copied() && <text>Successfully copied</text>}
|
||||
{copied() && <text fg={colors.muted}>Successfully copied</text>}
|
||||
</box>
|
||||
<box flexDirection="row" gap={2} alignItems="center">
|
||||
<text>A fatal error occurred!</text>
|
||||
<box onMouseUp={props.reset} backgroundColor="#565f89" padding={1}>
|
||||
<text>Reset TUI</text>
|
||||
<text fg={colors.text}>A fatal error occurred!</text>
|
||||
<box onMouseUp={props.reset} backgroundColor={colors.primary} padding={1}>
|
||||
<text fg={colors.bg}>Reset TUI</text>
|
||||
</box>
|
||||
<box onMouseUp={props.onExit} backgroundColor="#565f89" padding={1}>
|
||||
<text>Exit</text>
|
||||
<box onMouseUp={props.onExit} backgroundColor={colors.primary} padding={1}>
|
||||
<text fg={colors.bg}>Exit</text>
|
||||
</box>
|
||||
</box>
|
||||
<scrollbox height={Math.floor(term().height * 0.7)}>
|
||||
<text>{props.error.stack}</text>
|
||||
<text fg={colors.muted}>{props.error.stack}</text>
|
||||
</scrollbox>
|
||||
<text>{props.error.message}</text>
|
||||
<text fg={colors.text}>{props.error.message}</text>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,12 +14,17 @@ export const AttachCommand = cmd({
|
||||
.option("dir", {
|
||||
type: "string",
|
||||
description: "directory to run in",
|
||||
})
|
||||
.option("session", {
|
||||
alias: ["s"],
|
||||
type: "string",
|
||||
describe: "session id to continue",
|
||||
}),
|
||||
handler: async (args) => {
|
||||
if (args.dir) process.chdir(args.dir)
|
||||
await tui({
|
||||
url: args.url,
|
||||
args: {},
|
||||
args: { sessionID: args.session },
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -12,7 +12,7 @@ export function DialogAgent() {
|
||||
return {
|
||||
value: item.name,
|
||||
title: item.name,
|
||||
description: item.builtIn ? "native" : item.description,
|
||||
description: item.native ? "native" : item.description,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Keybind } from "@/util/keybind"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { useSDK } from "../context/sdk"
|
||||
import { DialogSessionRename } from "./dialog-session-rename"
|
||||
import "opentui-spinner/solid"
|
||||
|
||||
export function DialogSessionList() {
|
||||
const dialog = useDialog()
|
||||
@@ -22,6 +23,8 @@ export function DialogSessionList() {
|
||||
|
||||
const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
|
||||
|
||||
const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
||||
|
||||
const options = createMemo(() => {
|
||||
const today = new Date().toDateString()
|
||||
return sync.data.session
|
||||
@@ -34,12 +37,15 @@ export function DialogSessionList() {
|
||||
category = "Today"
|
||||
}
|
||||
const isDeleting = toDelete() === x.id
|
||||
const status = sync.data.session_status[x.id]
|
||||
const isWorking = status?.type === "busy"
|
||||
return {
|
||||
title: isDeleting ? `Press ${deleteKeybind} again to confirm` : x.title,
|
||||
bg: isDeleting ? theme.error : undefined,
|
||||
value: x.id,
|
||||
category,
|
||||
footer: Locale.time(x.time.updated),
|
||||
gutter: isWorking ? <spinner frames={spinnerFrames} interval={80} color={theme.primary} /> : undefined,
|
||||
}
|
||||
})
|
||||
.slice(0, 150)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Installation } from "@/installation"
|
||||
import { TextAttributes } from "@opentui/core"
|
||||
import { For } from "solid-js"
|
||||
import { useTheme } from "@tui/context/theme"
|
||||
@@ -14,8 +13,10 @@ export function Logo() {
|
||||
<For each={LOGO_LEFT}>
|
||||
{(line, index) => (
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text fg={theme.textMuted}>{line}</text>
|
||||
<text fg={theme.text} attributes={TextAttributes.BOLD}>
|
||||
<text fg={theme.textMuted} selectable={false}>
|
||||
{line}
|
||||
</text>
|
||||
<text fg={theme.text} attributes={TextAttributes.BOLD} selectable={false}>
|
||||
{LOGO_RIGHT[index()]}
|
||||
</text>
|
||||
</box>
|
||||
|
||||
@@ -184,7 +184,7 @@ export function Autocomplete(props: {
|
||||
const agents = createMemo(() => {
|
||||
const agents = sync.data.agent
|
||||
return agents
|
||||
.filter((agent) => !agent.builtIn && agent.mode !== "primary")
|
||||
.filter((agent) => !agent.hidden && agent.mode !== "primary")
|
||||
.map(
|
||||
(agent): AutocompleteOption => ({
|
||||
display: "@" + agent.name,
|
||||
@@ -357,13 +357,20 @@ export function Autocomplete(props: {
|
||||
|
||||
const options = createMemo(() => {
|
||||
const mixed: AutocompleteOption[] = (
|
||||
store.visible === "@" ? [...agents(), ...(files.loading ? files.latest || [] : files())] : [...commands()]
|
||||
store.visible === "@" ? [...agents(), ...(files() || [])] : [...commands()]
|
||||
).filter((x) => x.disabled !== true)
|
||||
const currentFilter = filter()
|
||||
if (!currentFilter) return mixed.slice(0, 10)
|
||||
const result = fuzzysort.go(currentFilter, mixed, {
|
||||
keys: [(obj) => obj.display.trimEnd(), "description", (obj) => obj.aliases?.join(" ") ?? ""],
|
||||
limit: 10,
|
||||
scoreFn: (objResults) => {
|
||||
const displayResult = objResults[0]
|
||||
if (displayResult && displayResult.target.startsWith(store.visible + currentFilter)) {
|
||||
return objResults.score * 2
|
||||
}
|
||||
return objResults.score
|
||||
},
|
||||
})
|
||||
return result.map((arr) => arr.obj)
|
||||
})
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { AgentPart, FilePart, TextPart } from "@opencode-ai/sdk/v2"
|
||||
|
||||
export type PromptInfo = {
|
||||
input: string
|
||||
mode?: "normal" | "shell"
|
||||
parts: (
|
||||
| Omit<FilePart, "id" | "messageID" | "sessionID">
|
||||
| Omit<AgentPart, "id" | "messageID" | "sessionID">
|
||||
|
||||
@@ -44,6 +44,7 @@ export type PromptRef = {
|
||||
reset(): void
|
||||
blur(): void
|
||||
focus(): void
|
||||
submit(): void
|
||||
}
|
||||
|
||||
const PLACEHOLDERS = ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"]
|
||||
@@ -447,11 +448,14 @@ export function Prompt(props: PromptProps) {
|
||||
})
|
||||
setStore("extmarkToPartIndex", new Map())
|
||||
},
|
||||
submit() {
|
||||
submit()
|
||||
},
|
||||
})
|
||||
|
||||
async function submit() {
|
||||
if (props.disabled) return
|
||||
if (autocomplete.visible) return
|
||||
if (autocomplete?.visible) return
|
||||
if (!store.prompt.input) return
|
||||
const trimmed = store.prompt.input.trim()
|
||||
if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") {
|
||||
@@ -491,6 +495,9 @@ export function Prompt(props: PromptProps) {
|
||||
// Filter out text parts (pasted content) since they're now expanded inline
|
||||
const nonTextParts = store.prompt.parts.filter((part) => part.type !== "text")
|
||||
|
||||
// Capture mode before it gets reset
|
||||
const currentMode = store.mode
|
||||
|
||||
if (store.mode === "shell") {
|
||||
sdk.client.session.shell({
|
||||
sessionID,
|
||||
@@ -539,7 +546,10 @@ export function Prompt(props: PromptProps) {
|
||||
],
|
||||
})
|
||||
}
|
||||
history.append(store.prompt)
|
||||
history.append({
|
||||
...store.prompt,
|
||||
mode: currentMode,
|
||||
})
|
||||
input.extmarks.clear()
|
||||
setStore("prompt", {
|
||||
input: "",
|
||||
@@ -705,8 +715,8 @@ export function Prompt(props: PromptProps) {
|
||||
>
|
||||
<textarea
|
||||
placeholder={props.sessionID ? undefined : `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
|
||||
textColor={theme.text}
|
||||
focusedTextColor={theme.text}
|
||||
textColor={keybind.leader ? theme.textMuted : theme.text}
|
||||
focusedTextColor={keybind.leader ? theme.textMuted : theme.text}
|
||||
minHeight={1}
|
||||
maxHeight={6}
|
||||
onContentChange={() => {
|
||||
@@ -732,8 +742,12 @@ export function Prompt(props: PromptProps) {
|
||||
return
|
||||
}
|
||||
if (keybind.match("app_exit", e)) {
|
||||
await exit()
|
||||
return
|
||||
if (store.prompt.input === "") {
|
||||
await exit()
|
||||
// Don't preventDefault - let textarea potentially handle the event
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
}
|
||||
if (e.name === "!" && input.visualCursor.offset === 0) {
|
||||
setStore("mode", "shell")
|
||||
@@ -759,6 +773,7 @@ export function Prompt(props: PromptProps) {
|
||||
if (item) {
|
||||
input.setText(item.input)
|
||||
setStore("prompt", item)
|
||||
setStore("mode", item.mode ?? "normal")
|
||||
restoreExtmarksFromParts(item.parts)
|
||||
e.preventDefault()
|
||||
if (direction === -1) input.cursorOffset = 0
|
||||
@@ -850,7 +865,7 @@ export function Prompt(props: PromptProps) {
|
||||
</text>
|
||||
<Show when={store.mode === "normal"}>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text flexShrink={0} fg={theme.text}>
|
||||
<text flexShrink={0} fg={keybind.leader ? theme.textMuted : theme.text}>
|
||||
{local.model.parsed().model}
|
||||
</text>
|
||||
<text fg={theme.textMuted}>{local.model.parsed().provider}</text>
|
||||
@@ -865,8 +880,7 @@ export function Prompt(props: PromptProps) {
|
||||
borderColor={highlight()}
|
||||
customBorderChars={{
|
||||
...EmptyBorder,
|
||||
// when the background is transparent, don't draw the vertical line
|
||||
vertical: theme.background.a != 0 ? "╹" : " ",
|
||||
vertical: theme.backgroundElement.a !== 0 ? "╹" : " ",
|
||||
}}
|
||||
>
|
||||
<box
|
||||
@@ -874,7 +888,7 @@ export function Prompt(props: PromptProps) {
|
||||
border={["bottom"]}
|
||||
borderColor={theme.backgroundElement}
|
||||
customBorderChars={
|
||||
theme.background.a != 0
|
||||
theme.backgroundElement.a !== 0
|
||||
? {
|
||||
...EmptyBorder,
|
||||
horizontal: "▀",
|
||||
|
||||
@@ -52,7 +52,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
||||
})
|
||||
|
||||
const agent = iife(() => {
|
||||
const agents = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent"))
|
||||
const agents = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden))
|
||||
const [agentStore, setAgentStore] = createStore<{
|
||||
current: string
|
||||
}>({
|
||||
|
||||
@@ -57,6 +57,7 @@ export function Home() {
|
||||
} else if (args.prompt) {
|
||||
prompt.set({ input: args.prompt, parts: [] })
|
||||
once = true
|
||||
prompt.submit()
|
||||
}
|
||||
})
|
||||
const directory = useDirectory()
|
||||
|
||||
@@ -5,8 +5,13 @@ import type { TextPart } from "@opencode-ai/sdk/v2"
|
||||
import { Locale } from "@/util/locale"
|
||||
import { DialogMessage } from "./dialog-message"
|
||||
import { useDialog } from "../../ui/dialog"
|
||||
import type { PromptInfo } from "../../component/prompt/history"
|
||||
|
||||
export function DialogTimeline(props: { sessionID: string; onMove: (messageID: string) => void }) {
|
||||
export function DialogTimeline(props: {
|
||||
sessionID: string
|
||||
onMove: (messageID: string) => void
|
||||
setPrompt?: (prompt: PromptInfo) => void
|
||||
}) {
|
||||
const sync = useSync()
|
||||
const dialog = useDialog()
|
||||
|
||||
@@ -26,10 +31,13 @@ export function DialogTimeline(props: { sessionID: string; onMove: (messageID: s
|
||||
value: message.id,
|
||||
footer: Locale.time(message.time.created),
|
||||
onSelect: (dialog) => {
|
||||
dialog.replace(() => <DialogMessage messageID={message.id} sessionID={props.sessionID} />)
|
||||
dialog.replace(() => (
|
||||
<DialogMessage messageID={message.id} sessionID={props.sessionID} setPrompt={props.setPrompt} />
|
||||
))
|
||||
},
|
||||
})
|
||||
}
|
||||
result.reverse()
|
||||
return result
|
||||
})
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user