Compare commits
209
Commits
docs
...
github-v1.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31e6ed6806 | ||
|
|
da56319af4 | ||
|
|
2198f9400f | ||
|
|
ffc4d53923 | ||
|
|
18d3c054a3 | ||
|
|
59c5da9b6c | ||
|
|
15880195a2 | ||
|
|
117de64f39 | ||
|
|
388156704a | ||
|
|
faf443132f | ||
|
|
36a9be040b | ||
|
|
1835d7526f | ||
|
|
946e4f0a61 | ||
|
|
ae60f41adf | ||
|
|
6b93d23642 | ||
|
|
cfa13df346 | ||
|
|
744a7159e4 | ||
|
|
80d1c62818 | ||
|
|
83aa42f510 | ||
|
|
183a1a181c | ||
|
|
bc7e7c2c4d | ||
|
|
7b5bd89570 | ||
|
|
ba1c6122b9 | ||
|
|
baed581a7c | ||
|
|
4a23052778 | ||
|
|
ee4190aa41 | ||
|
|
de8460cb99 | ||
|
|
f7b2beaaf1 | ||
|
|
9b0933187e | ||
|
|
862141e8b2 | ||
|
|
070ced0b3f | ||
|
|
cc3b699823 | ||
|
|
301f1a191b | ||
|
|
d149c25aab | ||
|
|
18d24b8f5f | ||
|
|
cf34981e8f | ||
|
|
e2ebe560ea | ||
|
|
6db822fd92 | ||
|
|
661122bab8 | ||
|
|
4a96836d11 | ||
|
|
e072f9605c | ||
|
|
9986031481 | ||
|
|
3d95848607 | ||
|
|
221c9028af | ||
|
|
b2057791aa | ||
|
|
c1ee6d6c41 | ||
|
|
a3fbbece9a | ||
|
|
e72c974c4c | ||
|
|
a762da7cab | ||
|
|
fa6c060324 | ||
|
|
8e33ac052b | ||
|
|
0759696ec0 | ||
|
|
59dce63471 | ||
|
|
1ae28090e3 | ||
|
|
0decdf6a55 | ||
|
|
09b402a274 | ||
|
|
150baf3e96 | ||
|
|
78c51371af | ||
|
|
6dbcacf3ea | ||
|
|
4ecebc2c83 | ||
|
|
38a79fa449 | ||
|
|
bafad6b8a8 | ||
|
|
5682dddd45 | ||
|
|
a9aacdb94a | ||
|
|
e7e32c946b | ||
|
|
fc9bc26d86 | ||
|
|
ee00b4e0ce | ||
|
|
f82156f0b1 | ||
|
|
2ed6298584 | ||
|
|
52ef8dea3e | ||
|
|
ebe6015db0 | ||
|
|
56526114e4 | ||
|
|
bb1c225027 | ||
|
|
e5af0dde08 | ||
|
|
3cf17bc24f | ||
|
|
4aa1b8de0e | ||
|
|
73e9534d08 | ||
|
|
cb188f907f | ||
|
|
63d9656ad8 | ||
|
|
3512d02e9e | ||
|
|
1efdceaf10 | ||
|
|
632a0fe009 | ||
|
|
6fb32cebec | ||
|
|
8b8b17d755 | ||
|
|
2c27afaaf5 | ||
|
|
4bdc7c1426 | ||
|
|
3c1e6c2c8f | ||
|
|
b8f5809f95 | ||
|
|
552ee81455 | ||
|
|
9fdbe193cd | ||
|
|
df64612d54 | ||
|
|
0aa3e6c270 | ||
|
|
44c17c1435 | ||
|
|
132e772c26 | ||
|
|
62cbed57cc | ||
|
|
ebab7e176e | ||
|
|
9c93853e22 | ||
|
|
8a9c7a4ef3 | ||
|
|
2dad56c9a2 | ||
|
|
41d78c1ecc | ||
|
|
16c4b02b69 | ||
|
|
35c04d9283 | ||
|
|
1fbd7a7f9a | ||
|
|
d7563d1694 | ||
|
|
b9fa7d9163 | ||
|
|
f736751ab2 | ||
|
|
dbcc779f0b | ||
|
|
c33a90320c | ||
|
|
802b862aae | ||
|
|
b0cd171c1b | ||
|
|
13755f4680 | ||
|
|
b242659cc3 | ||
|
|
5f6b2fdc6f | ||
|
|
e34f18991e | ||
|
|
209b0a06f7 | ||
|
|
a2e460bc4b | ||
|
|
fc9081afe4 | ||
|
|
1a3f7c3d84 | ||
|
|
06aa1f49b8 | ||
|
|
dfd67cd922 | ||
|
|
70f7287ca1 | ||
|
|
f1955b4d05 | ||
|
|
c5e5627cbd | ||
|
|
93378526b9 | ||
|
|
abf176a335 | ||
|
|
84a0868e66 | ||
|
|
75a9c42789 | ||
|
|
204fa54625 | ||
|
|
365584048f | ||
|
|
edffcc32cf | ||
|
|
238f441bcb | ||
|
|
0571a8302c | ||
|
|
8c07382382 | ||
|
|
fa32fbd187 | ||
|
|
0fd2ecd0ba | ||
|
|
7439a40b00 | ||
|
|
2ad99713f3 | ||
|
|
19ec970701 | ||
|
|
b48caec218 | ||
|
|
380c34af53 | ||
|
|
553d9013eb | ||
|
|
8bff3cdae8 | ||
|
|
0b40c3d37d | ||
|
|
1e3bdcc71c | ||
|
|
de577e17da | ||
|
|
8a9e258ad7 | ||
|
|
9a34965432 | ||
|
|
c944d19c3b | ||
|
|
fb1b6c5e6b | ||
|
|
ad0c4c5d89 | ||
|
|
a54b663a39 | ||
|
|
ae4993f39a | ||
|
|
aa638cec48 | ||
|
|
4db4a90559 | ||
|
|
e23a81097c | ||
|
|
76f4803d8d | ||
|
|
22e4649318 | ||
|
|
0ac70ff261 | ||
|
|
1bc1e56da3 | ||
|
|
0d0c20e673 | ||
|
|
a964824b22 | ||
|
|
2cf0d578fe | ||
|
|
13e8fb382f | ||
|
|
4090bc9dea | ||
|
|
cec1caf99e | ||
|
|
c74da97d52 | ||
|
|
1f2497ce69 | ||
|
|
986f14cb15 | ||
|
|
34f639d510 | ||
|
|
defe51c825 | ||
|
|
5a16acef8c | ||
|
|
2ce249dbc0 | ||
|
|
7ba6b18945 | ||
|
|
b8c0b393bf | ||
|
|
5442adb517 | ||
|
|
6b2ac20abc | ||
|
|
3efc95b157 | ||
|
|
cd9db8a81d | ||
|
|
036f5d4eef | ||
|
|
c4401290db | ||
|
|
4a6deb6420 | ||
|
|
87a03e1e30 | ||
|
|
01dc9d7ec6 | ||
|
|
e78e0f9841 | ||
|
|
8326640670 | ||
|
|
d079af4be2 | ||
|
|
82c9584382 | ||
|
|
d3b6de855b | ||
|
|
5ad000fd99 | ||
|
|
fe196da430 | ||
|
|
20662e2101 | ||
|
|
0a357be160 | ||
|
|
d29205e677 | ||
|
|
9d0630f094 | ||
|
|
b6844565e8 | ||
|
|
17d1b24def | ||
|
|
3d279edf44 | ||
|
|
0a47a3cea0 | ||
|
|
306d57fcde | ||
|
|
ff6f1abf61 | ||
|
|
331278a5be | ||
|
|
78547f3c59 | ||
|
|
d32671224f | ||
|
|
9ade416ad4 | ||
|
|
f8bd4ff705 | ||
|
|
2206e10d92 | ||
|
|
e282d5dc42 | ||
|
|
2b4a5aede1 | ||
|
|
654a2cd6a4 |
@@ -1,4 +1,4 @@
|
|||||||
name: format
|
name: generate
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -8,8 +8,9 @@ on:
|
|||||||
branches-ignore:
|
branches-ignore:
|
||||||
- production
|
- production
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
format:
|
generate:
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -24,9 +25,14 @@ jobs:
|
|||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: ./.github/actions/setup-bun
|
uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
- name: run
|
- name: Generate SDK
|
||||||
run: |
|
run: |
|
||||||
./script/format.ts
|
bun ./packages/sdk/js/script/build.ts
|
||||||
|
(cd packages/opencode && bun dev generate > ../sdk/openapi.json)
|
||||||
|
bun x prettier --write packages/sdk/openapi.json
|
||||||
|
|
||||||
|
- name: Format
|
||||||
|
run: ./script/format.ts
|
||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
PUSH_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }}
|
PUSH_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }}
|
||||||
@@ -2,11 +2,15 @@ name: publish
|
|||||||
run-name: "${{ format('release {0}', inputs.bump) }}"
|
run-name: "${{ format('release {0}', inputs.bump) }}"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
- snapshot-*
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
bump:
|
bump:
|
||||||
description: "Bump major, minor, or patch"
|
description: "Bump major, minor, or patch"
|
||||||
required: true
|
required: false
|
||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- major
|
- major
|
||||||
@@ -20,6 +24,7 @@ on:
|
|||||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
id-token: write
|
||||||
contents: write
|
contents: write
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
@@ -34,20 +39,13 @@ jobs:
|
|||||||
|
|
||||||
- run: git fetch --force --tags
|
- run: git fetch --force --tags
|
||||||
|
|
||||||
- uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: ">=1.24.0"
|
|
||||||
cache: true
|
|
||||||
cache-dependency-path: go.sum
|
|
||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
- uses: ./.github/actions/setup-bun
|
||||||
|
|
||||||
- name: Install makepkg
|
- name: Setup SSH for AUR
|
||||||
|
if: inputs.bump || inputs.version
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y pacman-package-manager
|
sudo apt-get install -y pacman-package-manager
|
||||||
- name: Setup SSH for AUR
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/.ssh
|
mkdir -p ~/.ssh
|
||||||
echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa
|
echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa
|
||||||
chmod 600 ~/.ssh/id_rsa
|
chmod 600 ~/.ssh/id_rsa
|
||||||
@@ -56,12 +54,9 @@ jobs:
|
|||||||
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
|
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
|
||||||
|
|
||||||
- name: Install OpenCode
|
- name: Install OpenCode
|
||||||
|
if: inputs.bump || inputs.version
|
||||||
run: curl -fsSL https://opencode.ai/install | bash
|
run: curl -fsSL https://opencode.ai/install | bash
|
||||||
|
|
||||||
- name: Setup npm auth
|
|
||||||
run: |
|
|
||||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
@@ -69,19 +64,24 @@ jobs:
|
|||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "24"
|
||||||
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
- name: Publish
|
- name: Publish
|
||||||
run: |
|
run: |
|
||||||
./script/publish.ts
|
./script/publish.ts
|
||||||
env:
|
env:
|
||||||
OPENCODE_BUMP: ${{ inputs.bump }}
|
OPENCODE_BUMP: ${{ inputs.bump }}
|
||||||
OPENCODE_VERSION: ${{ inputs.version }}
|
OPENCODE_VERSION: ${{ inputs.version }}
|
||||||
OPENCODE_CHANNEL: latest
|
|
||||||
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
|
||||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
|
||||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||||
|
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||||
|
NPM_CONFIG_PROVENANCE: false
|
||||||
|
|
||||||
publish-tauri:
|
publish-tauri:
|
||||||
|
if: false # inputs.bump || inputs.version
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
name: sdk
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches-ignore:
|
|
||||||
- production
|
|
||||||
pull_request:
|
|
||||||
branches-ignore:
|
|
||||||
- production
|
|
||||||
workflow_dispatch:
|
|
||||||
jobs:
|
|
||||||
format:
|
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Setup Bun
|
|
||||||
uses: ./.github/actions/setup-bun
|
|
||||||
|
|
||||||
- name: run
|
|
||||||
run: |
|
|
||||||
bun ./packages/sdk/js/script/build.ts
|
|
||||||
(cd packages/opencode && bun dev generate > ../sdk/openapi.json)
|
|
||||||
if [ -z "$(git status --porcelain)" ]; then
|
|
||||||
echo "No changes to commit"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
git config --local user.email "action@github.com"
|
|
||||||
git config --local user.name "GitHub Action"
|
|
||||||
git add -A
|
|
||||||
git commit -m "chore: regen sdk"
|
|
||||||
git push --no-verify
|
|
||||||
env:
|
|
||||||
CI: true
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
name: snapshot
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
- test-bedrock
|
|
||||||
- v0
|
|
||||||
- otui-diffs
|
|
||||||
- snapshot-*
|
|
||||||
|
|
||||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish:
|
|
||||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- run: git fetch --force --tags
|
|
||||||
|
|
||||||
- uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: ">=1.24.0"
|
|
||||||
cache: true
|
|
||||||
cache-dependency-path: go.sum
|
|
||||||
|
|
||||||
- uses: ./.github/actions/setup-bun
|
|
||||||
|
|
||||||
- name: Publish
|
|
||||||
run: |
|
|
||||||
./script/publish.ts
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
|
||||||
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
@@ -2,8 +2,8 @@ name: "sync-zed-extension"
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
release:
|
# release:
|
||||||
types: [published]
|
# types: [published]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
zed:
|
zed:
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ node_modules
|
|||||||
playground
|
playground
|
||||||
tmp
|
tmp
|
||||||
dist
|
dist
|
||||||
|
ts-dist
|
||||||
.turbo
|
.turbo
|
||||||
**/.serena
|
**/.serena
|
||||||
.serena/
|
.serena/
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
description: git commit and push
|
description: git commit and push
|
||||||
|
model: opencode/glm-4.6
|
||||||
---
|
---
|
||||||
|
|
||||||
commit and push
|
commit and push
|
||||||
|
|||||||
@@ -7,9 +7,7 @@
|
|||||||
"instructions": ["STYLE_GUIDE.md"],
|
"instructions": ["STYLE_GUIDE.md"],
|
||||||
"provider": {
|
"provider": {
|
||||||
"opencode": {
|
"opencode": {
|
||||||
"options": {
|
"options": {},
|
||||||
// "baseURL": "http://localhost:8080",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"mcp": {
|
"mcp": {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ scoop bucket add extras; scoop install extras/opencode # Windows
|
|||||||
choco install opencode # Windows
|
choco install opencode # Windows
|
||||||
brew install opencode # macOS and Linux
|
brew install opencode # macOS and Linux
|
||||||
paru -S opencode-bin # Arch Linux
|
paru -S opencode-bin # Arch Linux
|
||||||
mise use --pin -g ubi:sst/opencode # Any OS
|
mise use -g ubi:sst/opencode # Any OS
|
||||||
nix run nixpkgs#opencode # or github:sst/opencode for latest dev branch
|
nix run nixpkgs#opencode # or github:sst/opencode for latest dev branch
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Download Stats
|
# Download Stats
|
||||||
|
|
||||||
| Date | GitHub Downloads | npm Downloads | Total |
|
| Date | GitHub Downloads | npm Downloads | Total |
|
||||||
| ---------- | ------------------ | ----------------- | ------------------- |
|
| ---------- | ------------------- | ----------------- | ------------------- |
|
||||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||||
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
||||||
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
||||||
@@ -164,3 +164,5 @@
|
|||||||
| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) |
|
| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) |
|
||||||
| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) |
|
| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) |
|
||||||
| 2025-12-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) |
|
| 2025-12-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) |
|
||||||
|
| 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) |
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/app": {
|
"packages/console/app": {
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.0.137",
|
"version": "1.0.143",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "1.15.2",
|
"@cloudflare/vite-plugin": "1.15.2",
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/core": {
|
"packages/console/core": {
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.0.137",
|
"version": "1.0.143",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@jsx-email/render": "1.1.1",
|
"@jsx-email/render": "1.1.1",
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/function": {
|
"packages/console/function": {
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.0.137",
|
"version": "1.0.143",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/anthropic": "2.0.0",
|
"@ai-sdk/anthropic": "2.0.0",
|
||||||
"@ai-sdk/openai": "2.0.2",
|
"@ai-sdk/openai": "2.0.2",
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
},
|
},
|
||||||
"packages/console/mail": {
|
"packages/console/mail": {
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.0.137",
|
"version": "1.0.143",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.0.137",
|
"version": "1.0.143",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -168,7 +168,7 @@
|
|||||||
},
|
},
|
||||||
"packages/enterprise": {
|
"packages/enterprise": {
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.0.137",
|
"version": "1.0.143",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/ui": "workspace:*",
|
"@opencode-ai/ui": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
@@ -196,7 +196,7 @@
|
|||||||
},
|
},
|
||||||
"packages/function": {
|
"packages/function": {
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.0.137",
|
"version": "1.0.143",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "8.0.1",
|
"@octokit/auth-app": "8.0.1",
|
||||||
"@octokit/rest": "22.0.0",
|
"@octokit/rest": "22.0.0",
|
||||||
@@ -212,7 +212,7 @@
|
|||||||
},
|
},
|
||||||
"packages/opencode": {
|
"packages/opencode": {
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"version": "1.0.137",
|
"version": "1.0.143",
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode",
|
"opencode": "./bin/opencode",
|
||||||
},
|
},
|
||||||
@@ -241,9 +241,9 @@
|
|||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@openrouter/ai-sdk-provider": "1.2.8",
|
"@openrouter/ai-sdk-provider": "1.5.2",
|
||||||
"@opentui/core": "0.1.59",
|
"@opentui/core": "0.1.60",
|
||||||
"@opentui/solid": "0.1.59",
|
"@opentui/solid": "0.1.60",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@pierre/precision-diffs": "catalog:",
|
"@pierre/precision-diffs": "catalog:",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
@@ -304,7 +304,7 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.0.137",
|
"version": "1.0.143",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
@@ -324,7 +324,7 @@
|
|||||||
},
|
},
|
||||||
"packages/sdk/js": {
|
"packages/sdk/js": {
|
||||||
"name": "@opencode-ai/sdk",
|
"name": "@opencode-ai/sdk",
|
||||||
"version": "1.0.137",
|
"version": "1.0.143",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hey-api/openapi-ts": "0.88.1",
|
"@hey-api/openapi-ts": "0.88.1",
|
||||||
"@tsconfig/node22": "catalog:",
|
"@tsconfig/node22": "catalog:",
|
||||||
@@ -335,7 +335,7 @@
|
|||||||
},
|
},
|
||||||
"packages/slack": {
|
"packages/slack": {
|
||||||
"name": "@opencode-ai/slack",
|
"name": "@opencode-ai/slack",
|
||||||
"version": "1.0.137",
|
"version": "1.0.143",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@slack/bolt": "^3.17.1",
|
"@slack/bolt": "^3.17.1",
|
||||||
@@ -348,12 +348,13 @@
|
|||||||
},
|
},
|
||||||
"packages/tauri": {
|
"packages/tauri": {
|
||||||
"name": "@opencode-ai/tauri",
|
"name": "@opencode-ai/tauri",
|
||||||
"version": "1.0.137",
|
"version": "1.0.143",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/desktop": "workspace:*",
|
"@opencode-ai/desktop": "workspace:*",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-dialog": "~2",
|
"@tauri-apps/plugin-dialog": "~2",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
|
"@tauri-apps/plugin-process": "~2",
|
||||||
"@tauri-apps/plugin-shell": "~2",
|
"@tauri-apps/plugin-shell": "~2",
|
||||||
"@tauri-apps/plugin-updater": "~2",
|
"@tauri-apps/plugin-updater": "~2",
|
||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
@@ -362,13 +363,14 @@
|
|||||||
"@actions/artifact": "4.0.0",
|
"@actions/artifact": "4.0.0",
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
|
"@typescript/native-preview": "catalog:",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
"vite": "catalog:",
|
"vite": "catalog:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@opencode-ai/ui",
|
"name": "@opencode-ai/ui",
|
||||||
"version": "1.0.137",
|
"version": "1.0.143",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kobalte/core": "catalog:",
|
"@kobalte/core": "catalog:",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -400,7 +402,7 @@
|
|||||||
},
|
},
|
||||||
"packages/util": {
|
"packages/util": {
|
||||||
"name": "@opencode-ai/util",
|
"name": "@opencode-ai/util",
|
||||||
"version": "1.0.137",
|
"version": "1.0.143",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -411,7 +413,7 @@
|
|||||||
},
|
},
|
||||||
"packages/web": {
|
"packages/web": {
|
||||||
"name": "@opencode-ai/web",
|
"name": "@opencode-ai/web",
|
||||||
"version": "1.0.137",
|
"version": "1.0.143",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/cloudflare": "12.6.3",
|
"@astrojs/cloudflare": "12.6.3",
|
||||||
"@astrojs/markdown-remark": "6.3.1",
|
"@astrojs/markdown-remark": "6.3.1",
|
||||||
@@ -1139,27 +1141,27 @@
|
|||||||
|
|
||||||
"@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"],
|
"@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"],
|
||||||
|
|
||||||
"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@1.2.8", "", { "dependencies": { "@openrouter/sdk": "^0.1.8" }, "peerDependencies": { "ai": "^5.0.0", "zod": "^3.24.1 || ^v4" } }, "sha512-pQT8AzZBKg9f4bkt4doF486ZlhK0XjKkevrLkiqYgfh1Jplovieu28nK4Y+xy3sF18/mxjqh9/2y6jh01qzLrA=="],
|
"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@1.5.2", "", { "dependencies": { "@openrouter/sdk": "^0.1.27" }, "peerDependencies": { "@toon-format/toon": "^2.0.0", "ai": "^5.0.0", "zod": "^3.24.1 || ^v4" }, "optionalPeers": ["@toon-format/toon"] }, "sha512-3Th0vmJ9pjnwcPc2H1f59Mb0LFvwaREZAScfOQIpUxAHjZ7ZawVKDP27qgsteZPmMYqccNMy4r4Y3kgUnNcKAg=="],
|
||||||
|
|
||||||
"@openrouter/sdk": ["@openrouter/sdk@0.1.27", "", { "dependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-RH//L10bSmc81q25zAZudiI4kNkLgxF2E+WU42vghp3N6TEvZ6F0jK7uT3tOxkEn91gzmMw9YVmDENy7SJsajQ=="],
|
"@openrouter/sdk": ["@openrouter/sdk@0.1.27", "", { "dependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-RH//L10bSmc81q25zAZudiI4kNkLgxF2E+WU42vghp3N6TEvZ6F0jK7uT3tOxkEn91gzmMw9YVmDENy7SJsajQ=="],
|
||||||
|
|
||||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||||
|
|
||||||
"@opentui/core": ["@opentui/core@0.1.59", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.59", "@opentui/core-darwin-x64": "0.1.59", "@opentui/core-linux-arm64": "0.1.59", "@opentui/core-linux-x64": "0.1.59", "@opentui/core-win32-arm64": "0.1.59", "@opentui/core-win32-x64": "0.1.59", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-vOtEvIulvfCOWJy0EfKAPzAMtDTmC+S0boGYrefjLqIp7tp+bbVJuXVh/8bz6GQTPmbQC6MIk6bv/ij3pdUVkA=="],
|
"@opentui/core": ["@opentui/core@0.1.60", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.60", "@opentui/core-darwin-x64": "0.1.60", "@opentui/core-linux-arm64": "0.1.60", "@opentui/core-linux-x64": "0.1.60", "@opentui/core-win32-arm64": "0.1.60", "@opentui/core-win32-x64": "0.1.60", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-28jphd0AJo48uvEuKXcT9pJhgAu8I2rEJhPt25cc5ipJ2iw/eDk1uoxrbID80MPDqgOEzN21vXmzXwCd6ao+hg=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.59", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JQWq7W/wkmTujW/2/Ig0d7S+701rul87LSW5txQ+GM4o6EWchqHrELwo6jcZpczsyOEj4fXxI2O8l4OVYyMa9A=="],
|
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.60", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N4feqnOBDA4O4yocpat5vOiV06HqJVwJGx8rEZE9DiOtl1i+1cPQ1Lx6+zWdLhbrVBJ0ENhb7Azox8sXkm/+5Q=="],
|
||||||
|
|
||||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.59", "", { "os": "darwin", "cpu": "x64" }, "sha512-GzafWzMP9Lt4AzUwQAk02lxgITgfvvo33OLCN265LtQBO8w23u0eB7Fjs9W+nmtcvzXtB9q6HuA0PvP9a3OioA=="],
|
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.60", "", { "os": "darwin", "cpu": "x64" }, "sha512-+z3q4WaoIs7ANU8+eTFlvnfCjAS81rk81TOdZm4TJ53Ti3/B+yheWtnV/mLpLLhvZDz2VUVxxRmfDrGMnJb4fQ=="],
|
||||||
|
|
||||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.59", "", { "os": "linux", "cpu": "arm64" }, "sha512-QMMFg3dr2v43g3jICgzNFYQyU4YL3zHw733MVJINC+c882+qiQ8l0utTFoVEx/iRYeBzFvMVrKZ4f6G8fFrtrw=="],
|
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.60", "", { "os": "linux", "cpu": "arm64" }, "sha512-/Q65sjqVGB9ygJ6lStI8n1X6RyfmJZC8XofRGEuFiMLiWcWC/xoBtztdL8LAIvHQy42y2+pl9zIiW0fWSQ0wjw=="],
|
||||||
|
|
||||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.59", "", { "os": "linux", "cpu": "x64" }, "sha512-XSblVjhW/7+Xs+/o+xJHwHn74nw9j69mnPAFiNdH0d8ilP4j09nUYHZOvQ89sHZaMYeSIuJEciHnh/qP0n5QXQ=="],
|
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.60", "", { "os": "linux", "cpu": "x64" }, "sha512-AegF+g7OguIpjZKN+PS55sc3ZFY6fj+fLwfETbSRGw6NqX+aiwpae0Y3gXX1s298Yq5yQEzMXnARTCJTGH4uzg=="],
|
||||||
|
|
||||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.59", "", { "os": "win32", "cpu": "arm64" }, "sha512-GU5pPUcTpYmeOUYKpQgAPx0VKBMrfz5LNZlK8gm/jlo2CbLrIW7QLMWCoxncVZmNYqYJeG+KUZkmXYe5KLPXCQ=="],
|
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.60", "", { "os": "win32", "cpu": "arm64" }, "sha512-fbkq8MOZJgT3r9q3JWqsfVxRpQ1SlbmhmvB35BzukXnZBK8eA178wbSadGH6irMDrkSIYye9WYddHI/iXjmgVQ=="],
|
||||||
|
|
||||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.59", "", { "os": "win32", "cpu": "x64" }, "sha512-InIawEI0TOG8MBBpavMq31WBRBjJ6XPuqFcsDnjqDJcXrRbNkguRW3PNXEwlyaU4tXHfYOsdlPpRtsysS8X/bQ=="],
|
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.60", "", { "os": "win32", "cpu": "x64" }, "sha512-OebCL7f9+CKodBw0G+NvKIcc74bl6/sBEHfb73cACdJDJKh+T3C3Vt9H3kQQ0m1C8wRAqX6rh706OArk1pUb2A=="],
|
||||||
|
|
||||||
"@opentui/solid": ["@opentui/solid@0.1.59", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.59", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-O88a/+YHkHlDC4IxbrfWD2ZWlpkpu4oXC2FCLTK8taaUAnLYoybxdrMpv1+o8u8KoWXOoZmEHdntdO9O4abHnQ=="],
|
"@opentui/solid": ["@opentui/solid@0.1.60", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.60", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-pn91stzAHNGWaNL6h39q55bq3G1/DLqxKtT3wVsRAV68dHfPpwmqikX1nEJZK8OU84ZTPS9Ly9fz8po2Mot2uQ=="],
|
||||||
|
|
||||||
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
|
||||||
|
|
||||||
@@ -1655,6 +1657,8 @@
|
|||||||
|
|
||||||
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew=="],
|
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew=="],
|
||||||
|
|
||||||
|
"@tauri-apps/plugin-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-shell": ["@tauri-apps/plugin-shell@2.3.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Xod+pRcFxmOWFWEnqH5yZcA7qwAMuaaDkMR1Sply+F8VfBj++CGnj2xf5UoialmjZ2Cvd8qrvSCbU+7GgNVsKQ=="],
|
||||||
|
|
||||||
"@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-updater": ["@tauri-apps/plugin-updater@2.9.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg=="],
|
||||||
|
|||||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1764947035,
|
"lastModified": 1765270179,
|
||||||
"narHash": "sha256-EYHSjVM4Ox4lvCXUMiKKs2vETUSL5mx+J2FfutM7T9w=",
|
"narHash": "sha256-g2a4MhRKu4ymR4xwo+I+auTknXt/+j37Lnf0Mvfl1rE=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "a672be65651c80d3f592a89b3945466584a22069",
|
"rev": "677fbe97984e7af3175b6c121f3c39ee5c8d62c9",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"nodeModules": "sha256-IzF5XDY09Z1p/8jgYIHhE/jpKPub15KKUpV+a/aKpuc="
|
"nodeModules": "sha256-JT8J+Nd2kk0x46BcyotmBbM39tuKOW7VzXfOV3R3sqQ="
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
{ lib, stdenvNoCC, bun, fzf, ripgrep, makeBinaryWrapper }:
|
{ lib, stdenvNoCC, bun, ripgrep, makeBinaryWrapper }:
|
||||||
args:
|
args:
|
||||||
let
|
let
|
||||||
scripts = args.scripts;
|
scripts = args.scripts;
|
||||||
@@ -97,7 +97,7 @@ stdenvNoCC.mkDerivation (finalAttrs: {
|
|||||||
makeWrapper ${bun}/bin/bun $out/bin/opencode \
|
makeWrapper ${bun}/bin/bun $out/bin/opencode \
|
||||||
--add-flags "run" \
|
--add-flags "run" \
|
||||||
--add-flags "$out/lib/opencode/dist/src/index.js" \
|
--add-flags "$out/lib/opencode/dist/src/index.js" \
|
||||||
--prefix PATH : ${lib.makeBinPath [ fzf ripgrep ]} \
|
--prefix PATH : ${lib.makeBinPath [ ripgrep ]} \
|
||||||
--argv0 opencode
|
--argv0 opencode
|
||||||
|
|
||||||
runHook postInstall
|
runHook postInstall
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-app",
|
"name": "@opencode-ai/console-app",
|
||||||
"version": "1.0.137",
|
"version": "1.0.143",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo --noEmit",
|
||||||
|
|||||||
@@ -588,7 +588,7 @@ export async function handler(
|
|||||||
tx
|
tx
|
||||||
.update(KeyTable)
|
.update(KeyTable)
|
||||||
.set({ timeUsed: sql`now()` })
|
.set({ timeUsed: sql`now()` })
|
||||||
.where(eq(KeyTable.id, authInfo.apiKeyId)),
|
.where(and(eq(KeyTable.workspaceID, authInfo.workspaceID), eq(KeyTable.id, authInfo.apiKeyId))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/console-core",
|
"name": "@opencode-ai/console-core",
|
||||||
"version": "1.0.137",
|
"version": "1.0.143",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-function",
|
"name": "@opencode-ai/console-function",
|
||||||
"version": "1.0.137",
|
"version": "1.0.143",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/console-mail",
|
"name": "@opencode-ai/console-mail",
|
||||||
"version": "1.0.137",
|
"version": "1.0.143",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jsx-email/all": "2.2.3",
|
"@jsx-email/all": "2.2.3",
|
||||||
"@jsx-email/cli": "1.4.3",
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/desktop",
|
"name": "@opencode-ai/desktop",
|
||||||
"version": "1.0.137",
|
"version": "1.0.143",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
"./vite": "./vite.js"
|
"./vite": "./vite.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo -b",
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import { createContext } from "solid-js"
|
|
||||||
import { useContext } from "solid-js"
|
|
||||||
|
|
||||||
export interface Platform {}
|
|
||||||
|
|
||||||
const PlatformContext = createContext<Platform>()
|
|
||||||
|
|
||||||
export const PlatformProvider = PlatformContext.Provider
|
|
||||||
|
|
||||||
export function usePlatform() {
|
|
||||||
const ctx = useContext(PlatformContext)
|
|
||||||
if (!ctx) throw new Error("usePlatform must be used within a PlatformProvider")
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
@@ -2,19 +2,18 @@ import "@/index.css"
|
|||||||
import { Router, Route, Navigate } from "@solidjs/router"
|
import { Router, Route, Navigate } from "@solidjs/router"
|
||||||
import { MetaProvider } from "@solidjs/meta"
|
import { MetaProvider } from "@solidjs/meta"
|
||||||
import { Font } from "@opencode-ai/ui/font"
|
import { Font } from "@opencode-ai/ui/font"
|
||||||
import { Favicon } from "@opencode-ai/ui/favicon"
|
|
||||||
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
|
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
|
||||||
import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
|
import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
|
||||||
import { Diff } from "@opencode-ai/ui/diff"
|
import { Diff } from "@opencode-ai/ui/diff"
|
||||||
import { GlobalSyncProvider, useGlobalSync } from "./context/global-sync"
|
import { GlobalSyncProvider } from "./context/global-sync"
|
||||||
import Layout from "@/pages/layout"
|
import Layout from "@/pages/layout"
|
||||||
|
import Home from "@/pages/home"
|
||||||
import DirectoryLayout from "@/pages/directory-layout"
|
import DirectoryLayout from "@/pages/directory-layout"
|
||||||
import Session from "@/pages/session"
|
import Session from "@/pages/session"
|
||||||
import { LayoutProvider } from "./context/layout"
|
import { LayoutProvider } from "./context/layout"
|
||||||
import { GlobalSDKProvider } from "./context/global-sdk"
|
import { GlobalSDKProvider } from "./context/global-sdk"
|
||||||
import { SessionProvider } from "./context/session"
|
import { SessionProvider } from "./context/session"
|
||||||
import { base64Encode } from "@opencode-ai/util/encode"
|
import { Show } from "solid-js"
|
||||||
import { createMemo, Show } from "solid-js"
|
|
||||||
|
|
||||||
const host = import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "127.0.0.1"
|
const host = import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "127.0.0.1"
|
||||||
const port = import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"
|
const port = import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"
|
||||||
@@ -25,7 +24,7 @@ const url =
|
|||||||
? `http://${host}:${port}`
|
? `http://${host}:${port}`
|
||||||
: "/")
|
: "/")
|
||||||
|
|
||||||
export function DesktopInterface() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
<MarkedProvider>
|
<MarkedProvider>
|
||||||
<DiffComponentProvider component={Diff}>
|
<DiffComponentProvider component={Diff}>
|
||||||
@@ -35,14 +34,7 @@ export function DesktopInterface() {
|
|||||||
<MetaProvider>
|
<MetaProvider>
|
||||||
<Font />
|
<Font />
|
||||||
<Router root={Layout}>
|
<Router root={Layout}>
|
||||||
<Route
|
<Route path="/" component={Home} />
|
||||||
path="/"
|
|
||||||
component={() => {
|
|
||||||
const globalSync = useGlobalSync()
|
|
||||||
const slug = createMemo(() => base64Encode(globalSync.data.defaultProject!.worktree))
|
|
||||||
return <Navigate href={`${slug()}/session`} />
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Route path="/:dir" component={DirectoryLayout}>
|
<Route path="/:dir" component={DirectoryLayout}>
|
||||||
<Route path="/" component={() => <Navigate href="session" />} />
|
<Route path="/" component={() => <Navigate href="session" />} />
|
||||||
<Route
|
<Route
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
||||||
import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Match } from "solid-js"
|
import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Match, createSignal } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { createFocusSignal } from "@solid-primitives/active-element"
|
import { createFocusSignal } from "@solid-primitives/active-element"
|
||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
@@ -14,14 +14,44 @@ import { Button } from "@opencode-ai/ui/button"
|
|||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import { Select } from "@opencode-ai/ui/select"
|
import { Select } from "@opencode-ai/ui/select"
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||||
|
import { type IconName } from "@opencode-ai/ui/icons/provider"
|
||||||
|
|
||||||
interface PromptInputProps {
|
interface PromptInputProps {
|
||||||
class?: string
|
class?: string
|
||||||
ref?: (el: HTMLDivElement) => void
|
ref?: (el: HTMLDivElement) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PLACEHOLDERS = [
|
||||||
|
"Fix a TODO in the codebase",
|
||||||
|
"What is the tech stack of this project?",
|
||||||
|
"Fix broken tests",
|
||||||
|
"Explain how authentication works",
|
||||||
|
"Find and fix security vulnerabilities",
|
||||||
|
"Add unit tests for the user service",
|
||||||
|
"Refactor this function to be more readable",
|
||||||
|
"What does this error mean?",
|
||||||
|
"Help me debug this issue",
|
||||||
|
"Generate API documentation",
|
||||||
|
"Optimize database queries",
|
||||||
|
"Add input validation",
|
||||||
|
"Create a new component for...",
|
||||||
|
"How do I deploy this project?",
|
||||||
|
"Review my code for best practices",
|
||||||
|
"Add error handling to this function",
|
||||||
|
"Explain this regex pattern",
|
||||||
|
"Convert this to TypeScript",
|
||||||
|
"Add logging throughout the codebase",
|
||||||
|
"What dependencies are outdated?",
|
||||||
|
"Help me write a migration script",
|
||||||
|
"Implement caching for this endpoint",
|
||||||
|
"Add pagination to this list",
|
||||||
|
"Create a CLI command for...",
|
||||||
|
"How do environment variables work here?",
|
||||||
|
]
|
||||||
|
|
||||||
export const PromptInput: Component<PromptInputProps> = (props) => {
|
export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
@@ -36,6 +66,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
popoverIsOpen: false,
|
popoverIsOpen: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [placeholder, setPlaceholder] = createSignal(Math.floor(Math.random() * PLACEHOLDERS.length))
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setPlaceholder((prev) => (prev + 1) % PLACEHOLDERS.length)
|
||||||
|
}, 6500)
|
||||||
|
onCleanup(() => clearInterval(interval))
|
||||||
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
session.id
|
session.id
|
||||||
editorRef.focus()
|
editorRef.focus()
|
||||||
@@ -68,7 +107,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
|
|
||||||
const handleFileSelect = (path: string | undefined) => {
|
const handleFileSelect = (path: string | undefined) => {
|
||||||
if (!path) return
|
if (!path) return
|
||||||
addPart({ type: "file", path, content: "@" + getFilename(path), start: 0, end: 0 })
|
addPart({ type: "file", path, content: "@" + path, start: 0, end: 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const { flat, active, onInput, onKeyDown, refetch } = useFilteredList<string>({
|
const { flat, active, onInput, onKeyDown, refetch } = useFilteredList<string>({
|
||||||
@@ -403,7 +442,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
<Show when={!session.prompt.dirty()}>
|
<Show when={!session.prompt.dirty()}>
|
||||||
<div class="absolute top-0 left-0 px-5 py-3 text-14-regular text-text-weak pointer-events-none">
|
<div class="absolute top-0 left-0 px-5 py-3 text-14-regular text-text-weak pointer-events-none">
|
||||||
Plan and build anything
|
Ask anything... "{PLACEHOLDERS[placeholder()]}"
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
@@ -449,7 +488,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
{(i) => (
|
{(i) => (
|
||||||
<div class="w-full flex items-center justify-between gap-x-3">
|
<div class="w-full flex items-center justify-between gap-x-3">
|
||||||
<div class="flex items-center gap-x-2.5 text-text-muted grow min-w-0">
|
<div class="flex items-center gap-x-2.5 text-text-muted grow min-w-0">
|
||||||
<img src={`https://models.dev/logos/${i.provider.id}.svg`} class="size-6 p-0.5 shrink-0" />
|
<ProviderIcon name={i.provider.id as IconName} class="size-6 p-0.5 shrink-0" />
|
||||||
<div class="flex gap-x-3 items-baseline flex-[1_0_0]">
|
<div class="flex gap-x-3 items-baseline flex-[1_0_0]">
|
||||||
<span class="text-14-medium text-text-strong overflow-hidden text-ellipsis">{i.name}</span>
|
<span class="text-14-medium text-text-strong overflow-hidden text-ellipsis">{i.name}</span>
|
||||||
<Show when={false}>
|
<Show when={false}>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
|
|||||||
sdk.global.event().then(async (events) => {
|
sdk.global.event().then(async (events) => {
|
||||||
for await (const event of events.stream) {
|
for await (const event of events.stream) {
|
||||||
// console.log("event", event)
|
// console.log("event", event)
|
||||||
emitter.emit(event.directory, event.payload)
|
emitter.emit(event.directory ?? "global", event.payload)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -18,11 +18,43 @@ import { Binary } from "@opencode-ai/util/binary"
|
|||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { useGlobalSDK } from "./global-sdk"
|
import { useGlobalSDK } from "./global-sdk"
|
||||||
|
|
||||||
|
const PASTEL_COLORS = [
|
||||||
|
"#FCEAFD", // pastel pink
|
||||||
|
"#FFDFBA", // pastel peach
|
||||||
|
"#FFFFBA", // pastel yellow
|
||||||
|
"#BAFFC9", // pastel green
|
||||||
|
"#EAF6FD", // pastel blue
|
||||||
|
"#EFEAFD", // pastel lavender
|
||||||
|
"#FEC8D8", // pastel rose
|
||||||
|
"#D4F0F0", // pastel cyan
|
||||||
|
"#FDF0EA", // pastel coral
|
||||||
|
"#C1E1C1", // pastel mint
|
||||||
|
]
|
||||||
|
|
||||||
|
function pickAvailableColor(usedColors: Set<string>) {
|
||||||
|
const available = PASTEL_COLORS.filter((c) => !usedColors.has(c))
|
||||||
|
if (available.length === 0) return PASTEL_COLORS[Math.floor(Math.random() * PASTEL_COLORS.length)]
|
||||||
|
return available[Math.floor(Math.random() * available.length)]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureProjectColor(
|
||||||
|
project: Project,
|
||||||
|
sdk: ReturnType<typeof useGlobalSDK>,
|
||||||
|
usedColors: Set<string>,
|
||||||
|
): Promise<Project> {
|
||||||
|
if (project.icon?.color) return project
|
||||||
|
const color = pickAvailableColor(usedColors)
|
||||||
|
usedColors.add(color)
|
||||||
|
const updated = { ...project, icon: { ...project.icon, color } }
|
||||||
|
sdk.client.project.update({ projectID: project.id, icon: { color } })
|
||||||
|
return updated
|
||||||
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
ready: boolean
|
ready: boolean
|
||||||
provider: Provider[]
|
provider: Provider[]
|
||||||
agent: Agent[]
|
agent: Agent[]
|
||||||
project: Project
|
project: string
|
||||||
config: Config
|
config: Config
|
||||||
path: Path
|
path: Path
|
||||||
session: Session[]
|
session: Session[]
|
||||||
@@ -51,7 +83,6 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
|
|||||||
init: () => {
|
init: () => {
|
||||||
const [globalStore, setGlobalStore] = createStore<{
|
const [globalStore, setGlobalStore] = createStore<{
|
||||||
ready: boolean
|
ready: boolean
|
||||||
defaultProject?: Project // TODO: remove this when we can select projects
|
|
||||||
projects: Project[]
|
projects: Project[]
|
||||||
children: Record<string, State>
|
children: Record<string, State>
|
||||||
}>({
|
}>({
|
||||||
@@ -61,11 +92,10 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
|
|||||||
})
|
})
|
||||||
|
|
||||||
const children: Record<string, ReturnType<typeof createStore<State>>> = {}
|
const children: Record<string, ReturnType<typeof createStore<State>>> = {}
|
||||||
|
|
||||||
function child(directory: string) {
|
function child(directory: string) {
|
||||||
if (!children[directory]) {
|
if (!children[directory]) {
|
||||||
setGlobalStore("children", directory, {
|
setGlobalStore("children", directory, {
|
||||||
project: { id: "", worktree: "", time: { created: 0, initialized: 0 } },
|
project: "",
|
||||||
config: {},
|
config: {},
|
||||||
path: { state: "", config: "", worktree: "", directory: "" },
|
path: { state: "", config: "", worktree: "", directory: "" },
|
||||||
ready: false,
|
ready: false,
|
||||||
@@ -75,7 +105,7 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
|
|||||||
session_status: {},
|
session_status: {},
|
||||||
session_diff: {},
|
session_diff: {},
|
||||||
todo: {},
|
todo: {},
|
||||||
limit: 10,
|
limit: 5,
|
||||||
message: {},
|
message: {},
|
||||||
part: {},
|
part: {},
|
||||||
node: [],
|
node: [],
|
||||||
@@ -89,9 +119,32 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
|
|||||||
const sdk = useGlobalSDK()
|
const sdk = useGlobalSDK()
|
||||||
sdk.event.listen((e) => {
|
sdk.event.listen((e) => {
|
||||||
const directory = e.name
|
const directory = e.name
|
||||||
const [store, setStore] = child(directory)
|
|
||||||
|
|
||||||
const event = e.details
|
const event = e.details
|
||||||
|
|
||||||
|
if (directory === "global") {
|
||||||
|
switch (event.type) {
|
||||||
|
case "project.updated": {
|
||||||
|
const usedColors = new Set(globalStore.projects.map((p) => p.icon?.color).filter(Boolean) as string[])
|
||||||
|
ensureProjectColor(event.properties, sdk, usedColors).then((project) => {
|
||||||
|
const result = Binary.search(globalStore.projects, project.id, (s) => s.id)
|
||||||
|
if (result.found) {
|
||||||
|
setGlobalStore("projects", result.index, reconcile(project))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setGlobalStore(
|
||||||
|
"projects",
|
||||||
|
produce((draft) => {
|
||||||
|
draft.splice(result.index, 0, project)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const [store, setStore] = child(directory)
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "session.updated": {
|
case "session.updated": {
|
||||||
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
|
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
|
||||||
@@ -162,14 +215,15 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
|
|||||||
})
|
})
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
sdk.client.project.list().then((x) =>
|
sdk.client.project.list().then(async (x) => {
|
||||||
|
const filtered = x.data!.filter((p) => !p.worktree.includes("opencode-test") && p.vcs)
|
||||||
|
const usedColors = new Set(filtered.map((p) => p.icon?.color).filter(Boolean) as string[])
|
||||||
|
const projects = await Promise.all(filtered.map((p) => ensureProjectColor(p, sdk, usedColors)))
|
||||||
setGlobalStore(
|
setGlobalStore(
|
||||||
"projects",
|
"projects",
|
||||||
x.data!.filter((x) => !x.worktree.includes("opencode-test")),
|
projects.sort((a, b) => a.id.localeCompare(b.id)),
|
||||||
),
|
)
|
||||||
),
|
}),
|
||||||
// TODO: remove this when we can select projects
|
|
||||||
sdk.client.project.current().then((x) => setGlobalStore("defaultProject", x.data)),
|
|
||||||
]).then(() => setGlobalStore("ready", true))
|
]).then(() => setGlobalStore("ready", true))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { createMemo } from "solid-js"
|
import { createMemo, onMount } from "solid-js"
|
||||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
import { makePersisted } from "@solid-primitives/storage"
|
import { makePersisted } from "@solid-primitives/storage"
|
||||||
import { useGlobalSync } from "./global-sync"
|
import { useGlobalSync } from "./global-sync"
|
||||||
|
import { useGlobalSDK } from "./global-sdk"
|
||||||
|
|
||||||
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
|
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
|
||||||
name: "Layout",
|
name: "Layout",
|
||||||
init: () => {
|
init: () => {
|
||||||
|
const globalSdk = useGlobalSDK()
|
||||||
const globalSync = useGlobalSync()
|
const globalSync = useGlobalSync()
|
||||||
const [store, setStore] = makePersisted(
|
const [store, setStore] = makePersisted(
|
||||||
createStore({
|
createStore({
|
||||||
projects: [] as { directory: string; expanded: boolean }[],
|
projects: [] as { worktree: string; expanded: boolean }[],
|
||||||
sidebar: {
|
sidebar: {
|
||||||
opened: true,
|
opened: false,
|
||||||
width: 280,
|
width: 280,
|
||||||
},
|
},
|
||||||
terminal: {
|
terminal: {
|
||||||
@@ -24,29 +26,66 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "____default-layout",
|
name: "default-layout.v6",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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(({ worktree }) => {
|
||||||
|
return loadProjectSessions(worktree)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
function enrich(project: { worktree: string; expanded: boolean }) {
|
||||||
|
const metadata = globalSync.data.projects.find((x) => x.worktree === project.worktree)
|
||||||
|
if (!metadata) return []
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...project,
|
||||||
|
...metadata,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
projects: {
|
projects: {
|
||||||
list: createMemo(() =>
|
list: createMemo(() => store.projects.flatMap(enrich)),
|
||||||
globalSync.data.defaultProject
|
|
||||||
? [{ directory: globalSync.data.defaultProject!.worktree, expanded: true }, ...store.projects]
|
|
||||||
: store.projects,
|
|
||||||
),
|
|
||||||
open(directory: string) {
|
open(directory: string) {
|
||||||
if (store.projects.find((x) => x.directory === directory)) return
|
if (store.projects.find((x) => x.worktree === directory)) return
|
||||||
setStore("projects", (x) => [...x, { directory, expanded: true }])
|
loadProjectSessions(directory)
|
||||||
|
setStore("projects", (x) => [...x, { worktree: directory, expanded: true }])
|
||||||
},
|
},
|
||||||
close(directory: string) {
|
close(directory: string) {
|
||||||
setStore("projects", (x) => x.filter((x) => x.directory !== directory))
|
setStore("projects", (x) => x.filter((x) => x.worktree !== directory))
|
||||||
},
|
},
|
||||||
expand(directory: string) {
|
expand(directory: string) {
|
||||||
setStore("projects", (x) => x.map((x) => (x.directory === directory ? { ...x, expanded: true } : x)))
|
setStore("projects", (x) => x.map((x) => (x.worktree === directory ? { ...x, expanded: true } : x)))
|
||||||
},
|
},
|
||||||
collapse(directory: string) {
|
collapse(directory: string) {
|
||||||
setStore("projects", (x) => x.map((x) => (x.directory === directory ? { ...x, expanded: false } : x)))
|
setStore("projects", (x) => x.map((x) => (x.worktree === directory ? { ...x, expanded: false } : x)))
|
||||||
|
},
|
||||||
|
move(directory: string, toIndex: number) {
|
||||||
|
setStore("projects", (projects) => {
|
||||||
|
const fromIndex = projects.findIndex((x) => x.worktree === directory)
|
||||||
|
if (fromIndex === -1 || fromIndex === toIndex) return projects
|
||||||
|
const result = [...projects]
|
||||||
|
const [item] = result.splice(fromIndex, 1)
|
||||||
|
result.splice(toIndex, 0, item)
|
||||||
|
return result
|
||||||
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
sidebar: {
|
sidebar: {
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
|
|
||||||
const load = async (path: string) => {
|
const load = async (path: string) => {
|
||||||
const relativePath = relative(path)
|
const relativePath = relative(path)
|
||||||
sdk.client.file.read({ path: relativePath }).then((x) => {
|
await sdk.client.file.read({ path: relativePath }).then((x) => {
|
||||||
setStore(
|
setStore(
|
||||||
"node",
|
"node",
|
||||||
relativePath,
|
relativePath,
|
||||||
@@ -335,7 +335,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
node: async (path: string) => {
|
node: async (path: string) => {
|
||||||
if (!store.node[path]) {
|
if (!store.node[path] || !store.node[path].loaded) {
|
||||||
await init(path)
|
await init(path)
|
||||||
}
|
}
|
||||||
return store.node[path]
|
return store.node[path]
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||||
|
|
||||||
|
export type Platform = {
|
||||||
|
/** Platform discriminator */
|
||||||
|
platform: "web" | "tauri"
|
||||||
|
|
||||||
|
/** Open native directory picker dialog (Tauri only) */
|
||||||
|
openDirectoryPickerDialog?(opts?: { title?: string; multiple?: boolean }): Promise<string | string[] | null>
|
||||||
|
|
||||||
|
/** Open native file picker dialog (Tauri only) */
|
||||||
|
openFilePickerDialog?(opts?: { title?: string; multiple?: boolean }): Promise<string | string[] | null>
|
||||||
|
|
||||||
|
/** Save file picker dialog (Tauri only) */
|
||||||
|
saveFilePickerDialog?(opts?: { title?: string; defaultPath?: string }): Promise<string | null>
|
||||||
|
|
||||||
|
/** Open a URL in the default browser */
|
||||||
|
openLink(url: string): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({
|
||||||
|
name: "Platform",
|
||||||
|
init: (props: { value: Platform }) => {
|
||||||
|
return props.value
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -7,7 +7,6 @@ import { TextSelection } from "./local"
|
|||||||
import { pipe, sumBy } from "remeda"
|
import { pipe, sumBy } from "remeda"
|
||||||
import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2"
|
import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { base64Encode } from "@opencode-ai/util/encode"
|
|
||||||
import { useSDK } from "./sdk"
|
import { useSDK } from "./sdk"
|
||||||
|
|
||||||
export type LocalPTY = {
|
export type LocalPTY = {
|
||||||
@@ -25,9 +24,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
|
|||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const name = createMemo(
|
const name = createMemo(() => `${params.dir}/session${params.id ? "/" + params.id : ""}.v3`)
|
||||||
() => `${base64Encode(sync.data.project.worktree)}/session${params.id ? "/" + params.id : ""}.v2`,
|
|
||||||
)
|
|
||||||
|
|
||||||
const [store, setStore] = makePersisted(
|
const [store, setStore] = makePersisted(
|
||||||
createStore<{
|
createStore<{
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
const [store, setStore] = globalSync.child(sdk.directory)
|
const [store, setStore] = globalSync.child(sdk.directory)
|
||||||
|
|
||||||
const load = {
|
const load = {
|
||||||
project: () => sdk.client.project.current().then((x) => setStore("project", x.data!)),
|
project: () => sdk.client.project.current().then((x) => setStore("project", x.data!.id)),
|
||||||
provider: () => sdk.client.config.providers().then((x) => setStore("provider", x.data!.providers)),
|
provider: () => sdk.client.config.providers().then((x) => setStore("provider", x.data!.providers)),
|
||||||
path: () => sdk.client.path.get().then((x) => setStore("path", x.data!)),
|
path: () => sdk.client.path.get().then((x) => setStore("path", x.data!)),
|
||||||
agent: () => sdk.client.app.agents().then((x) => setStore("agent", x.data ?? [])),
|
agent: () => sdk.client.app.agents().then((x) => setStore("agent", x.data ?? [])),
|
||||||
@@ -41,6 +41,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
get ready() {
|
get ready() {
|
||||||
return store.ready
|
return store.ready
|
||||||
},
|
},
|
||||||
|
get project() {
|
||||||
|
const match = Binary.search(globalSync.data.projects, store.project, (p) => p.id)
|
||||||
|
if (match.found) return globalSync.data.projects[match.index]
|
||||||
|
return undefined
|
||||||
|
},
|
||||||
session: {
|
session: {
|
||||||
get(sessionID: string) {
|
get(sessionID: string) {
|
||||||
const match = Binary.search(store.session, sessionID, (s) => s.id)
|
const match = Binary.search(store.session, sessionID, (s) => s.id)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// @refresh reload
|
// @refresh reload
|
||||||
import { render } from "solid-js/web"
|
import { render } from "solid-js/web"
|
||||||
import { DesktopInterface } from "@/DesktopInterface"
|
import { App } from "@/app"
|
||||||
import { Platform, PlatformProvider } from "@/PlatformContext"
|
import { Platform, PlatformProvider } from "@/context/platform"
|
||||||
|
|
||||||
const root = document.getElementById("root")
|
const root = document.getElementById("root")
|
||||||
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
||||||
@@ -10,12 +10,17 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const platform: Platform = {}
|
const platform: Platform = {
|
||||||
|
platform: "web",
|
||||||
|
openLink(url: string) {
|
||||||
|
window.open(url, "_blank")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
render(
|
render(
|
||||||
() => (
|
() => (
|
||||||
<PlatformProvider value={platform}>
|
<PlatformProvider value={platform}>
|
||||||
<DesktopInterface />
|
<App />
|
||||||
</PlatformProvider>
|
</PlatformProvider>
|
||||||
),
|
),
|
||||||
root!,
|
root!,
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export { PlatformProvider, type Platform } from "./PlatformContext"
|
export { PlatformProvider, type Platform } from "./context/platform"
|
||||||
export { DesktopInterface } from "./DesktopInterface"
|
export { App } from "./app"
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
import { createMemo, type ParentProps } from "solid-js"
|
import { createMemo, Show, type ParentProps } from "solid-js"
|
||||||
import { useParams } from "@solidjs/router"
|
import { useParams } from "@solidjs/router"
|
||||||
import { SDKProvider } from "@/context/sdk"
|
import { SDKProvider } from "@/context/sdk"
|
||||||
import { SyncProvider, useSync } from "@/context/sync"
|
import { SyncProvider, useSync } from "@/context/sync"
|
||||||
import { LocalProvider } from "@/context/local"
|
import { LocalProvider } from "@/context/local"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
|
||||||
import { base64Decode } from "@opencode-ai/util/encode"
|
import { base64Decode } from "@opencode-ai/util/encode"
|
||||||
import { DataProvider } from "@opencode-ai/ui/context"
|
import { DataProvider } from "@opencode-ai/ui/context"
|
||||||
import { iife } from "@opencode-ai/util/iife"
|
import { iife } from "@opencode-ai/util/iife"
|
||||||
|
|
||||||
export default function Layout(props: ParentProps) {
|
export default function Layout(props: ParentProps) {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const sync = useGlobalSync()
|
|
||||||
const directory = createMemo(() => {
|
const directory = createMemo(() => {
|
||||||
const decoded = base64Decode(params.dir!)
|
return base64Decode(params.dir!)
|
||||||
return sync.data.projects.find((x) => x.worktree === decoded)?.worktree ?? "/"
|
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
|
<Show when={params.dir} keyed>
|
||||||
<SDKProvider directory={directory()}>
|
<SDKProvider directory={directory()}>
|
||||||
<SyncProvider>
|
<SyncProvider>
|
||||||
{iife(() => {
|
{iife(() => {
|
||||||
@@ -28,5 +26,6 @@ export default function Layout(props: ParentProps) {
|
|||||||
})}
|
})}
|
||||||
</SyncProvider>
|
</SyncProvider>
|
||||||
</SDKProvider>
|
</SDKProvider>
|
||||||
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,92 @@
|
|||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { base64Encode } from "@opencode-ai/util/encode"
|
import { For, Match, Show, Switch } from "solid-js"
|
||||||
import { For } from "solid-js"
|
|
||||||
import { A } from "@solidjs/router"
|
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
import { Logo } from "@opencode-ai/ui/logo"
|
||||||
|
import { useLayout } from "@/context/layout"
|
||||||
|
import { useNavigate } from "@solidjs/router"
|
||||||
|
import { base64Encode } from "@opencode-ai/util/encode"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
|
import { DateTime } from "luxon"
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const sync = useGlobalSync()
|
const sync = useGlobalSync()
|
||||||
|
const layout = useLayout()
|
||||||
|
const platform = usePlatform()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
function openProject(directory: string) {
|
||||||
|
layout.projects.open(directory)
|
||||||
|
navigate(`/${base64Encode(directory)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function chooseProject() {
|
||||||
|
const result = await platform.openDirectoryPickerDialog?.({
|
||||||
|
title: "Open project",
|
||||||
|
multiple: true,
|
||||||
|
})
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
for (const directory of result) {
|
||||||
|
openProject(directory)
|
||||||
|
}
|
||||||
|
} else if (result) {
|
||||||
|
openProject(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col gap-3">
|
<div class="mx-auto mt-55">
|
||||||
<For each={sync.data.projects}>
|
<Logo class="w-xl opacity-12" />
|
||||||
|
<Switch>
|
||||||
|
<Match when={sync.data.projects.length > 0}>
|
||||||
|
<div class="mt-20 w-full flex flex-col gap-4">
|
||||||
|
<div class="flex gap-2 items-center justify-between pl-3">
|
||||||
|
<div class="text-14-medium text-text-strong">Recent projects</div>
|
||||||
|
<Show when={platform.openDirectoryPickerDialog}>
|
||||||
|
<Button icon="folder-add-left" size="normal" class="pl-2 pr-3" onClick={chooseProject}>
|
||||||
|
Open project
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<ul class="flex flex-col gap-2">
|
||||||
|
<For
|
||||||
|
each={sync.data.projects
|
||||||
|
.toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
|
||||||
|
.slice(0, 5)}
|
||||||
|
>
|
||||||
{(project) => (
|
{(project) => (
|
||||||
<Button as={A} href={base64Encode(project.worktree)}>
|
<Button
|
||||||
{getFilename(project.worktree)}
|
size="large"
|
||||||
|
variant="ghost"
|
||||||
|
class="text-14-mono text-left justify-between px-3"
|
||||||
|
onClick={() => openProject(project.worktree)}
|
||||||
|
>
|
||||||
|
{project.worktree}
|
||||||
|
<div class="text-14-regular text-text-weak">
|
||||||
|
{DateTime.fromMillis(project.time.updated ?? project.time.created).toRelative()}
|
||||||
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
<Match when={true}>
|
||||||
|
<div class="mt-30 mx-auto flex flex-col items-center gap-3">
|
||||||
|
<Icon name="folder-add-left" size="large" />
|
||||||
|
<div class="flex flex-col gap-1 items-center justify-center">
|
||||||
|
<div class="text-14-medium text-text-strong">No recent projects</div>
|
||||||
|
<div class="text-12-regular text-text-weak">Get started by opening a local project</div>
|
||||||
|
</div>
|
||||||
|
<div />
|
||||||
|
<Show when={platform.openDirectoryPickerDialog}>
|
||||||
|
<Button class="px-3" onClick={chooseProject}>
|
||||||
|
Open project
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { createMemo, For, ParentProps, Show } from "solid-js"
|
import { createEffect, createMemo, For, Match, ParentProps, Show, Switch, type JSX } from "solid-js"
|
||||||
import { DateTime } from "luxon"
|
import { DateTime } from "luxon"
|
||||||
import { A, useNavigate, useParams } from "@solidjs/router"
|
import { A, useNavigate, useParams } from "@solidjs/router"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { useGlobalSync } from "@/context/global-sync"
|
import { useGlobalSync } from "@/context/global-sync"
|
||||||
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
|
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
|
||||||
import { Mark } from "@opencode-ai/ui/logo"
|
import { Mark } from "@opencode-ai/ui/logo"
|
||||||
|
import { Avatar } from "@opencode-ai/ui/avatar"
|
||||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
@@ -14,24 +15,296 @@ import { Collapsible } from "@opencode-ai/ui/collapsible"
|
|||||||
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
import { Select } from "@opencode-ai/ui/select"
|
import { Select } from "@opencode-ai/ui/select"
|
||||||
import { Session } from "@opencode-ai/sdk/v2/client"
|
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||||
|
import { Session, Project } from "@opencode-ai/sdk/v2/client"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import {
|
||||||
|
DragDropProvider,
|
||||||
|
DragDropSensors,
|
||||||
|
DragOverlay,
|
||||||
|
SortableProvider,
|
||||||
|
closestCenter,
|
||||||
|
createSortable,
|
||||||
|
useDragDropContext,
|
||||||
|
} from "@thisbeyond/solid-dnd"
|
||||||
|
import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
|
||||||
|
|
||||||
export default function Layout(props: ParentProps) {
|
export default function Layout(props: ParentProps) {
|
||||||
const navigate = useNavigate()
|
const [store, setStore] = createStore({
|
||||||
|
lastSession: {} as { [directory: string]: string },
|
||||||
|
activeDraggable: undefined as string | undefined,
|
||||||
|
})
|
||||||
|
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const globalSync = useGlobalSync()
|
const globalSync = useGlobalSync()
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
|
const platform = usePlatform()
|
||||||
|
const navigate = useNavigate()
|
||||||
const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
|
const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
|
||||||
const sessions = createMemo(() => globalSync.child(currentDirectory())[0].session ?? [])
|
const sessions = createMemo(() => globalSync.child(currentDirectory())[0].session ?? [])
|
||||||
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
|
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
|
||||||
|
|
||||||
|
function navigateToProject(directory: string | undefined) {
|
||||||
|
if (!directory) return
|
||||||
|
const lastSession = store.lastSession[directory]
|
||||||
|
navigate(`/${base64Encode(directory)}${lastSession ? `/session/${lastSession}` : ""}`)
|
||||||
|
}
|
||||||
|
|
||||||
function navigateToSession(session: Session | undefined) {
|
function navigateToSession(session: Session | undefined) {
|
||||||
if (!session) return
|
if (!session) return
|
||||||
navigate(`/${params.dir}/session/${session?.id}`)
|
navigate(`/${params.dir}/session/${session?.id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOpenProject = async () => {
|
function openProject(directory: string, navigate = true) {
|
||||||
// layout.projects.open(dir.)
|
layout.projects.open(directory)
|
||||||
|
if (navigate) navigateToProject(directory)
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeProject(directory: string) {
|
||||||
|
layout.projects.close(directory)
|
||||||
|
// TODO: more intelligent navigation
|
||||||
|
navigate("/")
|
||||||
|
}
|
||||||
|
|
||||||
|
async function chooseProject() {
|
||||||
|
const result = await platform.openDirectoryPickerDialog?.({
|
||||||
|
title: "Open project",
|
||||||
|
multiple: true,
|
||||||
|
})
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
for (const directory of result) {
|
||||||
|
openProject(directory, false)
|
||||||
|
}
|
||||||
|
navigateToProject(result[0])
|
||||||
|
} else if (result) {
|
||||||
|
openProject(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!params.dir || !params.id) return
|
||||||
|
const directory = base64Decode(params.dir)
|
||||||
|
setStore("lastSession", directory, params.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
function getDraggableId(event: unknown): string | undefined {
|
||||||
|
if (typeof event !== "object" || event === null) return undefined
|
||||||
|
if (!("draggable" in event)) return undefined
|
||||||
|
const draggable = (event as { draggable?: { id?: unknown } }).draggable
|
||||||
|
if (!draggable) return undefined
|
||||||
|
return typeof draggable.id === "string" ? draggable.id : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragStart(event: unknown) {
|
||||||
|
const id = getDraggableId(event)
|
||||||
|
if (!id) return
|
||||||
|
setStore("activeDraggable", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragOver(event: DragEvent) {
|
||||||
|
const { draggable, droppable } = event
|
||||||
|
if (draggable && droppable) {
|
||||||
|
const projects = layout.projects.list()
|
||||||
|
const fromIndex = projects.findIndex((p) => p.worktree === draggable.id.toString())
|
||||||
|
const toIndex = projects.findIndex((p) => p.worktree === droppable.id.toString())
|
||||||
|
if (fromIndex !== toIndex && toIndex !== -1) {
|
||||||
|
layout.projects.move(draggable.id.toString(), toIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragEnd() {
|
||||||
|
setStore("activeDraggable", undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConstrainDragXAxis = (): JSX.Element => {
|
||||||
|
const context = useDragDropContext()
|
||||||
|
if (!context) return <></>
|
||||||
|
const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = context
|
||||||
|
const transformer: Transformer = {
|
||||||
|
id: "constrain-x-axis",
|
||||||
|
order: 100,
|
||||||
|
callback: (transform) => ({ ...transform, x: 0 }),
|
||||||
|
}
|
||||||
|
onDragStart((event) => {
|
||||||
|
const id = getDraggableId(event)
|
||||||
|
if (!id) return
|
||||||
|
addTransformer("draggables", id, transformer)
|
||||||
|
})
|
||||||
|
onDragEnd((event) => {
|
||||||
|
const id = getDraggableId(event)
|
||||||
|
if (!id) return
|
||||||
|
removeTransformer("draggables", id, transformer.id)
|
||||||
|
})
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProjectVisual = (props: { project: Project & { expanded: boolean }; class?: string }): JSX.Element => {
|
||||||
|
const name = createMemo(() => getFilename(props.project.worktree))
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Match when={layout.sidebar.opened()}>
|
||||||
|
<Button
|
||||||
|
as={"div"}
|
||||||
|
variant="ghost"
|
||||||
|
data-active
|
||||||
|
class="flex items-center justify-between gap-3 w-full px-1 self-stretch h-8 border-none rounded-lg"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3 p-0 text-left min-w-0 grow">
|
||||||
|
<div class="size-6 shrink-0">
|
||||||
|
<Avatar
|
||||||
|
fallback={name()}
|
||||||
|
src={props.project.icon?.url}
|
||||||
|
background={props.project.icon?.color ?? "var(--surface-info-base)"}
|
||||||
|
class="size-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span class="truncate text-14-medium text-text-strong">{name()}</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</Match>
|
||||||
|
<Match when={true}>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="large"
|
||||||
|
class="flex items-center justify-center p-0 aspect-square border-none rounded-lg"
|
||||||
|
data-selected={props.project.worktree === currentDirectory()}
|
||||||
|
onClick={() => navigateToProject(props.project.worktree)}
|
||||||
|
>
|
||||||
|
<div class="size-6 shrink-0">
|
||||||
|
<Avatar
|
||||||
|
fallback={name()}
|
||||||
|
src={props.project.icon?.url}
|
||||||
|
background={props.project.icon?.color ?? "var(--surface-info-base)"}
|
||||||
|
class="size-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SortableProject = (props: { project: Project & { expanded: boolean } }): JSX.Element => {
|
||||||
|
const sortable = createSortable(props.project.worktree)
|
||||||
|
const [projectStore] = globalSync.child(props.project.worktree)
|
||||||
|
const slug = createMemo(() => base64Encode(props.project.worktree))
|
||||||
|
const name = createMemo(() => getFilename(props.project.worktree))
|
||||||
|
return (
|
||||||
|
// @ts-ignore
|
||||||
|
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
|
||||||
|
<Switch>
|
||||||
|
<Match when={layout.sidebar.opened()}>
|
||||||
|
<Collapsible variant="ghost" defaultOpen class="gap-2 shrink-0">
|
||||||
|
<Button
|
||||||
|
as={"div"}
|
||||||
|
variant="ghost"
|
||||||
|
class="group/session flex items-center justify-between gap-3 w-full px-1 self-stretch h-auto border-none rounded-lg"
|
||||||
|
>
|
||||||
|
<Collapsible.Trigger class="group/trigger flex items-center gap-3 p-0 text-left min-w-0 grow border-none">
|
||||||
|
<div class="size-6 shrink-0">
|
||||||
|
<Avatar
|
||||||
|
fallback={name()}
|
||||||
|
src={props.project.icon?.url}
|
||||||
|
background={props.project.icon?.color ?? "var(--surface-info-base)"}
|
||||||
|
class="size-full group-hover/session:hidden"
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
name="chevron-right"
|
||||||
|
size="large"
|
||||||
|
class="hidden size-full items-center justify-center text-text-subtle group-hover/session:flex group-data-[expanded]/trigger:rotate-90 transition-transform duration-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span class="truncate text-14-medium text-text-strong">{name()}</span>
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
<div class="flex invisible gap-1 items-center group-hover/session:visible has-[[data-expanded]]:visible">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenu.Trigger as={IconButton} icon="dot-grid" variant="ghost" />
|
||||||
|
<DropdownMenu.Portal>
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
<DropdownMenu.Item onSelect={() => closeProject(props.project.worktree)}>
|
||||||
|
<DropdownMenu.ItemLabel>Close Project</DropdownMenu.ItemLabel>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Portal>
|
||||||
|
</DropdownMenu>
|
||||||
|
<Tooltip placement="top" value="New session">
|
||||||
|
<IconButton as={A} href={`${slug()}/session`} icon="plus-small" variant="ghost" />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
<Collapsible.Content>
|
||||||
|
<nav class="hidden @[4rem]:flex w-full flex-col gap-1.5">
|
||||||
|
<For each={projectStore.session}>
|
||||||
|
{(session) => {
|
||||||
|
const updated = createMemo(() => DateTime.fromMillis(session.time.updated))
|
||||||
|
return (
|
||||||
|
<A
|
||||||
|
data-active={session.id === params.id}
|
||||||
|
href={`${slug()}/session/${session.id}`}
|
||||||
|
class="group/session focus:outline-none cursor-default"
|
||||||
|
>
|
||||||
|
<Tooltip placement="right" value={session.title}>
|
||||||
|
<div
|
||||||
|
class="w-full pl-4 pr-2 py-1 rounded-md
|
||||||
|
group-data-[active=true]/session:bg-surface-raised-base-hover
|
||||||
|
group-hover/session:bg-surface-raised-base-hover
|
||||||
|
group-focus/session:bg-surface-raised-base-hover"
|
||||||
|
>
|
||||||
|
<div class="flex items-center self-stretch gap-6 justify-between">
|
||||||
|
<span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
|
||||||
|
{session.title}
|
||||||
|
</span>
|
||||||
|
<span class="text-12-regular text-text-weak text-right whitespace-nowrap">
|
||||||
|
{Math.abs(updated().diffNow().as("seconds")) < 60
|
||||||
|
? "Now"
|
||||||
|
: updated()
|
||||||
|
.toRelative({
|
||||||
|
style: "short",
|
||||||
|
unit: ["days", "hours", "minutes"],
|
||||||
|
})
|
||||||
|
?.replace(" ago", "")
|
||||||
|
?.replace(/ days?/, "d")
|
||||||
|
?.replace(" min.", "m")
|
||||||
|
?.replace(" hr.", "h")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="hidden _flex justify-between items-center self-stretch">
|
||||||
|
<span class="text-12-regular text-text-weak">{`${session.summary?.files || "No"} file${session.summary?.files !== 1 ? "s" : ""} changed`}</span>
|
||||||
|
<Show when={session.summary}>{(summary) => <DiffChanges changes={summary()} />}</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</A>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</nav>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible>
|
||||||
|
</Match>
|
||||||
|
<Match when={true}>
|
||||||
|
<Tooltip placement="right" value={props.project.worktree}>
|
||||||
|
<ProjectVisual project={props.project} />
|
||||||
|
</Tooltip>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProjectDragOverlay = (): JSX.Element => {
|
||||||
|
const project = createMemo(() => layout.projects.list().find((p) => p.worktree === store.activeDraggable))
|
||||||
|
return (
|
||||||
|
<Show when={project()}>
|
||||||
|
{(p) => (
|
||||||
|
<div class="bg-background-base rounded-md">
|
||||||
|
<ProjectVisual project={p()} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -50,19 +323,30 @@ export default function Layout(props: ParentProps) {
|
|||||||
<Mark class="shrink-0" />
|
<Mark class="shrink-0" />
|
||||||
</A>
|
</A>
|
||||||
<div class="pl-4 px-6 flex items-center justify-between gap-4 w-full">
|
<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-3">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Select
|
<Select
|
||||||
options={layout.projects.list().map((project) => getFilename(project.directory))}
|
options={layout.projects.list().map((project) => project.worktree)}
|
||||||
current={getFilename(currentDirectory())}
|
current={currentDirectory()}
|
||||||
|
label={(x) => getFilename(x)}
|
||||||
|
onSelect={(x) => (x ? navigateToProject(x) : undefined)}
|
||||||
class="text-14-regular text-text-base"
|
class="text-14-regular text-text-base"
|
||||||
variant="ghost"
|
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>
|
<div class="text-text-weaker">/</div>
|
||||||
<Select
|
<Select
|
||||||
options={sessions()}
|
options={sessions()}
|
||||||
current={currentSession()}
|
current={currentSession()}
|
||||||
placeholder="Select session"
|
placeholder="New session"
|
||||||
label={(x) => x.title}
|
label={(x) => x.title}
|
||||||
value={(x) => x.id}
|
value={(x) => x.id}
|
||||||
onSelect={navigateToSession}
|
onSelect={navigateToSession}
|
||||||
@@ -70,9 +354,11 @@ export default function Layout(props: ParentProps) {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<Show when={currentSession()}>
|
||||||
<Button as={A} href={`/${params.dir}/session`} icon="plus-small">
|
<Button as={A} href={`/${params.dir}/session`} icon="plus-small">
|
||||||
New session
|
New session
|
||||||
</Button>
|
</Button>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@@ -105,6 +391,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="h-[calc(100vh-3rem)] flex">
|
<div class="h-[calc(100vh-3rem)] flex">
|
||||||
@@ -127,12 +414,12 @@ export default function Layout(props: ParentProps) {
|
|||||||
onCollapse={layout.sidebar.close}
|
onCollapse={layout.sidebar.close}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<div class="grow flex flex-col items-start self-stretch gap-4 p-2 min-h-0">
|
<div class="flex flex-col items-start self-stretch gap-4 p-2 min-h-0 overflow-hidden">
|
||||||
<Tooltip class="shrink-0" placement="right" value="Toggle sidebar" inactive={layout.sidebar.opened()}>
|
<Tooltip class="shrink-0" placement="right" value="Toggle sidebar" inactive={layout.sidebar.opened()}>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="large"
|
size="large"
|
||||||
class="group/sidebar-toggle shrink-0 w-full text-left justify-start"
|
class="group/sidebar-toggle shrink-0 w-full text-left justify-start rounded-lg"
|
||||||
onClick={layout.sidebar.toggle}
|
onClick={layout.sidebar.toggle}
|
||||||
>
|
>
|
||||||
<div class="relative -ml-px flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
<div class="relative -ml-px flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
|
||||||
@@ -159,103 +446,42 @@ export default function Layout(props: ParentProps) {
|
|||||||
</Show>
|
</Show>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<div class="flex flex-col justify-center items-start gap-4 self-stretch min-h-0">
|
<DragDropProvider
|
||||||
<div class="hidden @[4rem]:flex size-full flex-col grow overflow-y-auto no-scrollbar">
|
onDragStart={handleDragStart}
|
||||||
<For each={layout.projects.list()}>
|
onDragEnd={handleDragEnd}
|
||||||
{(project) => {
|
onDragOver={handleDragOver}
|
||||||
const [store] = globalSync.child(project.directory)
|
collisionDetector={closestCenter}
|
||||||
const slug = createMemo(() => base64Encode(project.directory))
|
|
||||||
return (
|
|
||||||
<Collapsible variant="ghost" defaultOpen class="gap-2">
|
|
||||||
<Button
|
|
||||||
as={"div"}
|
|
||||||
variant="ghost"
|
|
||||||
class="flex items-center justify-between gap-3 w-full h-8 pl-2 pr-2.25 self-stretch"
|
|
||||||
>
|
>
|
||||||
<Collapsible.Trigger class="p-0 text-left text-14-medium text-text-strong grow min-w-0 truncate">
|
<DragDropSensors />
|
||||||
{getFilename(project.directory)}
|
<ConstrainDragXAxis />
|
||||||
</Collapsible.Trigger>
|
<div class="w-full min-w-8 flex flex-col gap-2 min-h-0 overflow-y-auto no-scrollbar">
|
||||||
<IconButton as={A} href={`${slug()}/session`} icon="plus-small" size="normal" />
|
<SortableProvider ids={layout.projects.list().map((p) => p.worktree)}>
|
||||||
</Button>
|
<For each={layout.projects.list()}>{(project) => <SortableProject project={project} />}</For>
|
||||||
<Collapsible.Content>
|
</SortableProvider>
|
||||||
<nav class="w-full flex flex-col gap-1.5">
|
|
||||||
<For each={store.session}>
|
|
||||||
{(session) => {
|
|
||||||
const updated = createMemo(() => DateTime.fromMillis(session.time.updated))
|
|
||||||
return (
|
|
||||||
<A
|
|
||||||
data-active={session.id === params.id}
|
|
||||||
href={`${slug()}/session/${session.id}`}
|
|
||||||
class="group/session focus:outline-none cursor-default"
|
|
||||||
>
|
|
||||||
<Tooltip placement="right" value={session.title}>
|
|
||||||
<div
|
|
||||||
class="w-full px-2 py-1 rounded-md
|
|
||||||
group-data-[active=true]/session:bg-surface-raised-base-hover
|
|
||||||
group-hover/session:bg-surface-raised-base-hover
|
|
||||||
group-focus/session:bg-surface-raised-base-hover"
|
|
||||||
>
|
|
||||||
<div class="flex items-center self-stretch gap-6 justify-between">
|
|
||||||
<span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
|
|
||||||
{session.title}
|
|
||||||
</span>
|
|
||||||
<span class="text-12-regular text-text-weak text-right whitespace-nowrap">
|
|
||||||
{Math.abs(updated().diffNow().as("seconds")) < 60
|
|
||||||
? "Now"
|
|
||||||
: updated()
|
|
||||||
.toRelative({ style: "short", unit: ["days", "hours", "minutes"] })
|
|
||||||
?.replace(" ago", "")
|
|
||||||
?.replace(/ days?/, "d")
|
|
||||||
?.replace(" min.", "m")
|
|
||||||
?.replace(" hr.", "h")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="hidden _flex justify-between items-center self-stretch">
|
|
||||||
<span class="text-12-regular text-text-weak">{`${session.summary?.files || "No"} file${session.summary?.files !== 1 ? "s" : ""} changed`}</span>
|
|
||||||
<Show when={session.summary}>
|
|
||||||
{(summary) => <DiffChanges changes={summary()} />}
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</A>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</nav>
|
|
||||||
{/* <Show when={sync.session.more()}> */}
|
|
||||||
{/* <button */}
|
|
||||||
{/* class="shrink-0 self-start p-3 text-12-medium text-text-weak hover:text-text-strong" */}
|
|
||||||
{/* onClick={() => sync.session.fetch()} */}
|
|
||||||
{/* > */}
|
|
||||||
{/* Show more */}
|
|
||||||
{/* </button> */}
|
|
||||||
{/* </Show> */}
|
|
||||||
</Collapsible.Content>
|
|
||||||
</Collapsible>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<DragOverlay>
|
||||||
|
<ProjectDragOverlay />
|
||||||
|
</DragOverlay>
|
||||||
|
</DragDropProvider>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
|
<div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
|
||||||
|
<Show when={platform.openDirectoryPickerDialog}>
|
||||||
<Tooltip placement="right" value="Open project" inactive={layout.sidebar.opened()}>
|
<Tooltip placement="right" value="Open project" inactive={layout.sidebar.opened()}>
|
||||||
<Button
|
<Button
|
||||||
disabled
|
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg"
|
||||||
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px]"
|
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="large"
|
size="large"
|
||||||
icon="folder-add-left"
|
icon="folder-add-left"
|
||||||
onClick={handleOpenProject}
|
onClick={chooseProject}
|
||||||
>
|
>
|
||||||
<Show when={layout.sidebar.opened()}>Open project</Show>
|
<Show when={layout.sidebar.opened()}>Open project</Show>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
</Show>
|
||||||
<Tooltip placement="right" value="Settings" inactive={layout.sidebar.opened()}>
|
<Tooltip placement="right" value="Settings" inactive={layout.sidebar.opened()}>
|
||||||
<Button
|
<Button
|
||||||
disabled
|
disabled
|
||||||
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px]"
|
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="large"
|
size="large"
|
||||||
icon="settings-gear"
|
icon="settings-gear"
|
||||||
@@ -268,7 +494,7 @@ export default function Layout(props: ParentProps) {
|
|||||||
as={"a"}
|
as={"a"}
|
||||||
href="https://opencode.ai/desktop-feedback"
|
href="https://opencode.ai/desktop-feedback"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px]"
|
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="large"
|
size="large"
|
||||||
icon="bubble-5"
|
icon="bubble-5"
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
|
import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
|
||||||
import type { JSX } from "solid-js"
|
import type { JSX } from "solid-js"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useSession } from "@/context/session"
|
import { useSession, type LocalPTY } from "@/context/session"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||||
import { Terminal } from "@/components/terminal"
|
import { Terminal } from "@/components/terminal"
|
||||||
@@ -43,6 +43,7 @@ export default function Page() {
|
|||||||
clickTimer: undefined as number | undefined,
|
clickTimer: undefined as number | undefined,
|
||||||
fileSelectOpen: false,
|
fileSelectOpen: false,
|
||||||
activeDraggable: undefined as string | undefined,
|
activeDraggable: undefined as string | undefined,
|
||||||
|
activeTerminalDraggable: undefined as string | undefined,
|
||||||
})
|
})
|
||||||
let inputRef!: HTMLDivElement
|
let inputRef!: HTMLDivElement
|
||||||
|
|
||||||
@@ -178,6 +179,49 @@ export default function Page() {
|
|||||||
setStore("activeDraggable", undefined)
|
setStore("activeDraggable", undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleTerminalDragStart = (event: unknown) => {
|
||||||
|
const id = getDraggableId(event)
|
||||||
|
if (!id) return
|
||||||
|
setStore("activeTerminalDraggable", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
|
||||||
|
session.terminal.move(draggable.id.toString(), toIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTerminalDragEnd = () => {
|
||||||
|
setStore("activeTerminalDraggable", undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SortableTerminalTab = (props: { terminal: LocalPTY }): JSX.Element => {
|
||||||
|
const sortable = createSortable(props.terminal.id)
|
||||||
|
return (
|
||||||
|
// @ts-ignore
|
||||||
|
<div use:sortable classList={{ "h-full": true, "opacity-0": sortable.isActiveDraggable }}>
|
||||||
|
<div class="relative h-full">
|
||||||
|
<Tabs.Trigger
|
||||||
|
value={props.terminal.id}
|
||||||
|
closeButton={
|
||||||
|
session.terminal.all().length > 1 && (
|
||||||
|
<IconButton icon="close" variant="ghost" onClick={() => session.terminal.close(props.terminal.id)} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{props.terminal.title}
|
||||||
|
</Tabs.Trigger>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const FileVisual = (props: { file: LocalFile; active?: boolean }): JSX.Element => {
|
const FileVisual = (props: { file: LocalFile; active?: boolean }): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div class="flex items-center gap-x-1.5">
|
<div class="flex items-center gap-x-1.5">
|
||||||
@@ -220,7 +264,6 @@ export default function Page() {
|
|||||||
onTabClose: (tab: string) => void
|
onTabClose: (tab: string) => void
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const sortable = createSortable(props.tab)
|
const sortable = createSortable(props.tab)
|
||||||
|
|
||||||
const [file] = createResource(
|
const [file] = createResource(
|
||||||
() => props.tab,
|
() => props.tab,
|
||||||
async (tab) => {
|
async (tab) => {
|
||||||
@@ -230,7 +273,6 @@ export default function Page() {
|
|||||||
return undefined
|
return undefined
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
<div use:sortable classList={{ "h-full": true, "opacity-0": sortable.isActiveDraggable }}>
|
<div use:sortable classList={{ "h-full": true, "opacity-0": sortable.isActiveDraggable }}>
|
||||||
@@ -403,15 +445,19 @@ export default function Page() {
|
|||||||
<span class="text-text-strong">{getFilename(sync.data.path.directory)}</span>
|
<span class="text-text-strong">{getFilename(sync.data.path.directory)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Show when={sync.project}>
|
||||||
|
{(project) => (
|
||||||
<div class="flex justify-center items-center gap-3">
|
<div class="flex justify-center items-center gap-3">
|
||||||
<Icon name="pencil-line" size="small" />
|
<Icon name="pencil-line" size="small" />
|
||||||
<div class="text-12-medium text-text-weak">
|
<div class="text-12-medium text-text-weak">
|
||||||
Last modified
|
Last modified
|
||||||
<span class="text-text-strong">
|
<span class="text-text-strong">
|
||||||
{DateTime.fromMillis(sync.data.project.time.created).toRelative()}
|
{DateTime.fromMillis(project().time.updated ?? project().time.created).toRelative()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
@@ -576,7 +622,6 @@ export default function Page() {
|
|||||||
onOpenChange={(open) => setStore("fileSelectOpen", open)}
|
onOpenChange={(open) => setStore("fileSelectOpen", open)}
|
||||||
onSelect={(x) => {
|
onSelect={(x) => {
|
||||||
if (x) {
|
if (x) {
|
||||||
local.file.open(x)
|
|
||||||
return session.layout.openTab("file://" + x)
|
return session.layout.openTab("file://" + x)
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined
|
||||||
@@ -617,22 +662,19 @@ export default function Page() {
|
|||||||
onResize={layout.terminal.resize}
|
onResize={layout.terminal.resize}
|
||||||
onCollapse={layout.terminal.close}
|
onCollapse={layout.terminal.close}
|
||||||
/>
|
/>
|
||||||
|
<DragDropProvider
|
||||||
|
onDragStart={handleTerminalDragStart}
|
||||||
|
onDragEnd={handleTerminalDragEnd}
|
||||||
|
onDragOver={handleTerminalDragOver}
|
||||||
|
collisionDetector={closestCenter}
|
||||||
|
>
|
||||||
|
<DragDropSensors />
|
||||||
|
<ConstrainDragYAxis />
|
||||||
<Tabs variant="alt" value={session.terminal.active()} onChange={session.terminal.open}>
|
<Tabs variant="alt" value={session.terminal.active()} onChange={session.terminal.open}>
|
||||||
<Tabs.List class="h-10">
|
<Tabs.List class="h-10">
|
||||||
<For each={session.terminal.all()}>
|
<SortableProvider ids={session.terminal.all().map((t) => t.id)}>
|
||||||
{(terminal) => (
|
<For each={session.terminal.all()}>{(terminal) => <SortableTerminalTab terminal={terminal} />}</For>
|
||||||
<Tabs.Trigger
|
</SortableProvider>
|
||||||
value={terminal.id}
|
|
||||||
closeButton={
|
|
||||||
session.terminal.all().length > 1 && (
|
|
||||||
<IconButton icon="close" variant="ghost" onClick={() => session.terminal.close(terminal.id)} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{terminal.title}
|
|
||||||
</Tabs.Trigger>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
<div class="h-full flex items-center justify-center">
|
<div class="h-full flex items-center justify-center">
|
||||||
<Tooltip value="Open file" class="flex items-center">
|
<Tooltip value="Open file" class="flex items-center">
|
||||||
<IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={session.terminal.new} />
|
<IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={session.terminal.new} />
|
||||||
@@ -651,6 +693,23 @@ export default function Page() {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
<DragOverlay>
|
||||||
|
<Show when={store.activeTerminalDraggable}>
|
||||||
|
{(draggedId) => {
|
||||||
|
const terminal = createMemo(() => session.terminal.all().find((t) => t.id === draggedId()))
|
||||||
|
return (
|
||||||
|
<Show when={terminal()}>
|
||||||
|
{(t) => (
|
||||||
|
<div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular">
|
||||||
|
{t().title}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Show>
|
||||||
|
</DragOverlay>
|
||||||
|
</DragDropProvider>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/tsconfig",
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
@@ -11,7 +12,9 @@
|
|||||||
"jsxImportSource": "solid-js",
|
"jsxImportSource": "solid-js",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noEmit": true,
|
"noEmit": false,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"outDir": "ts-dist",
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/enterprise",
|
"name": "@opencode-ai/enterprise",
|
||||||
"version": "1.0.137",
|
"version": "1.0.143",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FileDiff, Message, Model, Part, Session, SessionStatus } from "@opencode-ai/sdk"
|
import { FileDiff, Message, Model, Part, Session } from "@opencode-ai/sdk/v2"
|
||||||
import { fn } from "@opencode-ai/util/fn"
|
import { fn } from "@opencode-ai/util/fn"
|
||||||
import { iife } from "@opencode-ai/util/iife"
|
import { iife } from "@opencode-ai/util/iife"
|
||||||
import { Identifier } from "@opencode-ai/util/identifier"
|
import { Identifier } from "@opencode-ai/util/identifier"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FileDiff, Message, Model, Part, Session, SessionStatus, UserMessage } from "@opencode-ai/sdk"
|
import { FileDiff, Message, Model, Part, Session, SessionStatus, UserMessage } from "@opencode-ai/sdk/v2"
|
||||||
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
||||||
import { SessionReview } from "@opencode-ai/ui/session-review"
|
import { SessionReview } from "@opencode-ai/ui/session-review"
|
||||||
import { DataProvider } from "@opencode-ai/ui/context"
|
import { DataProvider } from "@opencode-ai/ui/context"
|
||||||
@@ -8,6 +8,7 @@ import { createEffect, createMemo, ErrorBoundary, For, Match, Show, Switch } fro
|
|||||||
import { Share } from "~/core/share"
|
import { Share } from "~/core/share"
|
||||||
import { Logo, Mark } from "@opencode-ai/ui/logo"
|
import { Logo, Mark } from "@opencode-ai/ui/logo"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import { createDefaultOptions } from "@opencode-ai/ui/pierre"
|
import { createDefaultOptions } from "@opencode-ai/ui/pierre"
|
||||||
import { iife } from "@opencode-ai/util/iife"
|
import { iife } from "@opencode-ai/util/iife"
|
||||||
import { Binary } from "@opencode-ai/util/binary"
|
import { Binary } from "@opencode-ai/util/binary"
|
||||||
@@ -21,6 +22,7 @@ import { Tabs } from "@opencode-ai/ui/tabs"
|
|||||||
import { preloadMultiFileDiff, PreloadMultiFileDiffResult } from "@pierre/precision-diffs/ssr"
|
import { preloadMultiFileDiff, PreloadMultiFileDiffResult } from "@pierre/precision-diffs/ssr"
|
||||||
import { Diff as SSRDiff } from "@opencode-ai/ui/diff-ssr"
|
import { Diff as SSRDiff } from "@opencode-ai/ui/diff-ssr"
|
||||||
import { clientOnly } from "@solidjs/start"
|
import { clientOnly } from "@solidjs/start"
|
||||||
|
import { type IconName } from "@opencode-ai/ui/icons/provider"
|
||||||
|
|
||||||
const ClientOnlyDiff = clientOnly(() => import("@opencode-ai/ui/diff").then((m) => ({ default: m.Diff })))
|
const ClientOnlyDiff = clientOnly(() => import("@opencode-ai/ui/diff").then((m) => ({ default: m.Diff })))
|
||||||
|
|
||||||
@@ -210,10 +212,7 @@ export default function () {
|
|||||||
<div class="text-12-mono text-text-base">v{info().version}</div>
|
<div class="text-12-mono text-text-base">v{info().version}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<img
|
<ProviderIcon name={provider() as IconName} class="size-3.5 shrink-0 text-icon-strong-base" />
|
||||||
src={`https://models.dev/logos/${provider()}.svg`}
|
|
||||||
class="size-3.5 shrink-0 dark:invert"
|
|
||||||
/>
|
|
||||||
<div class="text-12-regular text-text-base">{model()?.name ?? modelID()}</div>
|
<div class="text-12-regular text-text-base">{model()?.name ?? modelID()}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-12-regular text-text-weaker">
|
<div class="text-12-regular text-text-weaker">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
id = "opencode"
|
id = "opencode"
|
||||||
name = "OpenCode"
|
name = "OpenCode"
|
||||||
description = "The open source coding agent."
|
description = "The open source coding agent."
|
||||||
version = "1.0.137"
|
version = "1.0.143"
|
||||||
schema_version = 1
|
schema_version = 1
|
||||||
authors = ["Anomaly"]
|
authors = ["Anomaly"]
|
||||||
repository = "https://github.com/sst/opencode"
|
repository = "https://github.com/sst/opencode"
|
||||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
|||||||
icon = "./icons/opencode.svg"
|
icon = "./icons/opencode.svg"
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-aarch64]
|
[agent_servers.opencode.targets.darwin-aarch64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.137/opencode-darwin-arm64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.143/opencode-darwin-arm64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.darwin-x86_64]
|
[agent_servers.opencode.targets.darwin-x86_64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.137/opencode-darwin-x64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.143/opencode-darwin-x64.zip"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-aarch64]
|
[agent_servers.opencode.targets.linux-aarch64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.137/opencode-linux-arm64.tar.gz"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.143/opencode-linux-arm64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.linux-x86_64]
|
[agent_servers.opencode.targets.linux-x86_64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.137/opencode-linux-x64.tar.gz"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.143/opencode-linux-x64.tar.gz"
|
||||||
cmd = "./opencode"
|
cmd = "./opencode"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|
||||||
[agent_servers.opencode.targets.windows-x86_64]
|
[agent_servers.opencode.targets.windows-x86_64]
|
||||||
archive = "https://github.com/sst/opencode/releases/download/v1.0.137/opencode-windows-x64.zip"
|
archive = "https://github.com/sst/opencode/releases/download/v1.0.143/opencode-windows-x64.zip"
|
||||||
cmd = "./opencode.exe"
|
cmd = "./opencode.exe"
|
||||||
args = ["acp"]
|
args = ["acp"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@opencode-ai/function",
|
"name": "@opencode-ai/function",
|
||||||
"version": "1.0.137",
|
"version": "1.0.143",
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"version": "1.0.137",
|
"version": "1.0.143",
|
||||||
"name": "opencode",
|
"name": "opencode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -9,7 +9,12 @@
|
|||||||
"test": "bun test",
|
"test": "bun test",
|
||||||
"build": "./script/build.ts",
|
"build": "./script/build.ts",
|
||||||
"dev": "bun run --conditions=browser ./src/index.ts",
|
"dev": "bun run --conditions=browser ./src/index.ts",
|
||||||
"random": "echo 'Random script updated at $(date)'"
|
"random": "echo 'Random script updated at $(date)' && echo 'Change queued successfully' && echo 'Another change made' && echo 'Yet another change' && echo 'One more change' && echo 'Final change' && echo 'Another final change' && echo 'Yet another final change'",
|
||||||
|
"clean": "echo 'Cleaning up...' && rm -rf node_modules dist",
|
||||||
|
"lint": "echo 'Running lint checks...' && bun test --coverage",
|
||||||
|
"format": "echo 'Formatting code...' && bun run --prettier --write src/**/*.ts",
|
||||||
|
"docs": "echo 'Generating documentation...' && find src -name '*.ts' -exec echo 'Processing: {}' \\;",
|
||||||
|
"deploy": "echo 'Deploying application...' && bun run build && echo 'Deployment completed successfully'"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"opencode": "./bin/opencode"
|
"opencode": "./bin/opencode"
|
||||||
@@ -65,9 +70,9 @@
|
|||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
"@opencode-ai/util": "workspace:*",
|
"@opencode-ai/util": "workspace:*",
|
||||||
"@openrouter/ai-sdk-provider": "1.2.8",
|
"@openrouter/ai-sdk-provider": "1.5.2",
|
||||||
"@opentui/core": "0.1.59",
|
"@opentui/core": "0.1.60",
|
||||||
"@opentui/solid": "0.1.59",
|
"@opentui/solid": "0.1.60",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
"@pierre/precision-diffs": "catalog:",
|
"@pierre/precision-diffs": "catalog:",
|
||||||
"@solid-primitives/event-bus": "1.1.2",
|
"@solid-primitives/event-bus": "1.1.2",
|
||||||
|
|||||||
@@ -35,26 +35,21 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
|
|||||||
2,
|
2,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
for (const [name] of Object.entries(binaries)) {
|
|
||||||
try {
|
|
||||||
process.chdir(`./dist/${name}`)
|
|
||||||
if (process.platform !== "win32") {
|
|
||||||
await $`chmod 755 -R .`
|
|
||||||
}
|
|
||||||
await $`bun publish --access public --tag ${Script.channel}`
|
|
||||||
} finally {
|
|
||||||
process.chdir(dir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await $`cd ./dist/${pkg.name} && bun publish --access public --tag ${Script.channel}`
|
|
||||||
|
|
||||||
if (!Script.preview) {
|
const tags = [Script.channel]
|
||||||
const major = Script.version.split(".")[0]
|
|
||||||
const majorTag = `latest-${major}`
|
const tasks = Object.entries(binaries).map(async ([name]) => {
|
||||||
for (const [name] of Object.entries(binaries)) {
|
if (process.platform !== "win32") {
|
||||||
await $`cd dist/${name} && npm dist-tag add ${name}@${Script.version} ${majorTag}`
|
await $`chmod 755 -R .`.cwd(`./dist/${name}`)
|
||||||
}
|
}
|
||||||
await $`cd ./dist/${pkg.name} && npm dist-tag add ${pkg.name}-ai@${Script.version} ${majorTag}`
|
await $`bun pm pack`.cwd(`./dist/${name}`)
|
||||||
|
for (const tag of tags) {
|
||||||
|
await $`npm publish *.tgz --access public --tag ${tag}`.cwd(`./dist/${name}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await Promise.all(tasks)
|
||||||
|
for (const tag of tags) {
|
||||||
|
await $`cd ./dist/${pkg.name} && bun pm pack && npm publish *.tgz --access public --tag ${tag}`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Script.preview) {
|
if (!Script.preview) {
|
||||||
@@ -90,7 +85,7 @@ if (!Script.preview) {
|
|||||||
"license=('MIT')",
|
"license=('MIT')",
|
||||||
"provides=('opencode')",
|
"provides=('opencode')",
|
||||||
"conflicts=('opencode')",
|
"conflicts=('opencode')",
|
||||||
"depends=('fzf' 'ripgrep')",
|
"depends=('ripgrep')",
|
||||||
"",
|
"",
|
||||||
`source_aarch64=("\${pkgname}_\${pkgver}_aarch64.tar.gz::https://github.com/sst/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-arm64.tar.gz")`,
|
`source_aarch64=("\${pkgname}_\${pkgver}_aarch64.tar.gz::https://github.com/sst/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-arm64.tar.gz")`,
|
||||||
`sha256sums_aarch64=('${arm64Sha}')`,
|
`sha256sums_aarch64=('${arm64Sha}')`,
|
||||||
@@ -120,7 +115,7 @@ if (!Script.preview) {
|
|||||||
"license=('MIT')",
|
"license=('MIT')",
|
||||||
"provides=('opencode')",
|
"provides=('opencode')",
|
||||||
"conflicts=('opencode-bin')",
|
"conflicts=('opencode-bin')",
|
||||||
"depends=('fzf' 'ripgrep')",
|
"depends=('ripgrep')",
|
||||||
"makedepends=('git' 'bun-bin' 'go')",
|
"makedepends=('git' 'bun-bin' 'go')",
|
||||||
"",
|
"",
|
||||||
`source=("opencode-\${pkgver}.tar.gz::https://github.com/sst/opencode/archive/v\${pkgver}\${_subver}.tar.gz")`,
|
`source=("opencode-\${pkgver}.tar.gz::https://github.com/sst/opencode/archive/v\${pkgver}\${_subver}.tar.gz")`,
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import { Provider } from "../provider/provider"
|
|||||||
import { Installation } from "@/installation"
|
import { Installation } from "@/installation"
|
||||||
import { MessageV2 } from "@/session/message-v2"
|
import { MessageV2 } from "@/session/message-v2"
|
||||||
import { Config } from "@/config/config"
|
import { Config } from "@/config/config"
|
||||||
import { MCP } from "@/mcp"
|
|
||||||
import { Todo } from "@/session/todo"
|
import { Todo } from "@/session/todo"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { LoadAPIKeyError } from "ai"
|
import { LoadAPIKeyError } from "ai"
|
||||||
|
|||||||
@@ -245,9 +245,14 @@ export namespace Agent {
|
|||||||
system.push(PROMPT_GENERATE)
|
system.push(PROMPT_GENERATE)
|
||||||
const existing = await list()
|
const existing = await list()
|
||||||
const result = await generateObject({
|
const result = await generateObject({
|
||||||
experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry },
|
experimental_telemetry: {
|
||||||
|
isEnabled: cfg.experimental?.openTelemetry,
|
||||||
|
metadata: {
|
||||||
|
userId: cfg.username ?? "unknown",
|
||||||
|
},
|
||||||
|
},
|
||||||
temperature: 0.3,
|
temperature: 0.3,
|
||||||
prompt: [
|
messages: [
|
||||||
...system.map(
|
...system.map(
|
||||||
(item): ModelMessage => ({
|
(item): ModelMessage => ({
|
||||||
role: "system",
|
role: "system",
|
||||||
|
|||||||
@@ -127,7 +127,18 @@ export namespace BunProc {
|
|||||||
|
|
||||||
await runInstall()
|
await runInstall()
|
||||||
|
|
||||||
parsed.dependencies[pkg] = version
|
// Resolve actual version from installed package when using "latest"
|
||||||
|
// This ensures subsequent starts use the cached version until explicitly updated
|
||||||
|
let resolvedVersion = version
|
||||||
|
if (version === "latest") {
|
||||||
|
const installedPkgJson = Bun.file(path.join(mod, "package.json"))
|
||||||
|
const installedPkg = await installedPkgJson.json().catch(() => null)
|
||||||
|
if (installedPkg?.version) {
|
||||||
|
resolvedVersion = installedPkg.version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed.dependencies[pkg] = resolvedVersion
|
||||||
await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2))
|
await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2))
|
||||||
return mod
|
return mod
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import z from "zod"
|
||||||
|
import type { ZodType } from "zod"
|
||||||
|
import { Log } from "../util/log"
|
||||||
|
|
||||||
|
export namespace BusEvent {
|
||||||
|
const log = Log.create({ service: "event" })
|
||||||
|
|
||||||
|
export type Definition = ReturnType<typeof define>
|
||||||
|
|
||||||
|
const registry = new Map<string, Definition>()
|
||||||
|
|
||||||
|
export function define<Type extends string, Properties extends ZodType>(type: Type, properties: Properties) {
|
||||||
|
const result = {
|
||||||
|
type,
|
||||||
|
properties,
|
||||||
|
}
|
||||||
|
registry.set(type, result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function payloads() {
|
||||||
|
return z
|
||||||
|
.discriminatedUnion(
|
||||||
|
"type",
|
||||||
|
registry
|
||||||
|
.entries()
|
||||||
|
.map(([type, def]) => {
|
||||||
|
return z
|
||||||
|
.object({
|
||||||
|
type: z.literal(type),
|
||||||
|
properties: def.properties,
|
||||||
|
})
|
||||||
|
.meta({
|
||||||
|
ref: "Event" + "." + def.type,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.toArray() as any,
|
||||||
|
)
|
||||||
|
.meta({
|
||||||
|
ref: "Event",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import { EventEmitter } from "events"
|
|||||||
export const GlobalBus = new EventEmitter<{
|
export const GlobalBus = new EventEmitter<{
|
||||||
event: [
|
event: [
|
||||||
{
|
{
|
||||||
directory: string
|
directory?: string
|
||||||
payload: any
|
payload: any
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
import type { ZodType } from "zod"
|
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
|
import { BusEvent } from "./bus-event"
|
||||||
import { GlobalBus } from "./global"
|
import { GlobalBus } from "./global"
|
||||||
|
|
||||||
export namespace Bus {
|
export namespace Bus {
|
||||||
@@ -9,10 +9,6 @@ export namespace Bus {
|
|||||||
type Subscription = (event: any) => void
|
type Subscription = (event: any) => void
|
||||||
const disposedEventType = "server.instance.disposed"
|
const disposedEventType = "server.instance.disposed"
|
||||||
|
|
||||||
export type EventDefinition = ReturnType<typeof event>
|
|
||||||
|
|
||||||
const registry = new Map<string, EventDefinition>()
|
|
||||||
|
|
||||||
const state = Instance.state(
|
const state = Instance.state(
|
||||||
() => {
|
() => {
|
||||||
const subscriptions = new Map<any, Subscription[]>()
|
const subscriptions = new Map<any, Subscription[]>()
|
||||||
@@ -36,46 +32,14 @@ export namespace Bus {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
export function event<Type extends string, Properties extends ZodType>(type: Type, properties: Properties) {
|
export const InstanceDisposed = BusEvent.define(
|
||||||
const result = {
|
|
||||||
type,
|
|
||||||
properties,
|
|
||||||
}
|
|
||||||
registry.set(type, result)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
export const InstanceDisposed = event(
|
|
||||||
disposedEventType,
|
disposedEventType,
|
||||||
z.object({
|
z.object({
|
||||||
directory: z.string(),
|
directory: z.string(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export function payloads() {
|
export async function publish<Definition extends BusEvent.Definition>(
|
||||||
return z
|
|
||||||
.discriminatedUnion(
|
|
||||||
"type",
|
|
||||||
registry
|
|
||||||
.entries()
|
|
||||||
.map(([type, def]) => {
|
|
||||||
return z
|
|
||||||
.object({
|
|
||||||
type: z.literal(type),
|
|
||||||
properties: def.properties,
|
|
||||||
})
|
|
||||||
.meta({
|
|
||||||
ref: "Event" + "." + def.type,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.toArray() as any,
|
|
||||||
)
|
|
||||||
.meta({
|
|
||||||
ref: "Event",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function publish<Definition extends EventDefinition>(
|
|
||||||
def: Definition,
|
def: Definition,
|
||||||
properties: z.output<Definition["properties"]>,
|
properties: z.output<Definition["properties"]>,
|
||||||
) {
|
) {
|
||||||
@@ -100,14 +64,14 @@ export namespace Bus {
|
|||||||
return Promise.all(pending)
|
return Promise.all(pending)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function subscribe<Definition extends EventDefinition>(
|
export function subscribe<Definition extends BusEvent.Definition>(
|
||||||
def: Definition,
|
def: Definition,
|
||||||
callback: (event: { type: Definition["type"]; properties: z.infer<Definition["properties"]> }) => void,
|
callback: (event: { type: Definition["type"]; properties: z.infer<Definition["properties"]> }) => void,
|
||||||
) {
|
) {
|
||||||
return raw(def.type, callback)
|
return raw(def.type, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function once<Definition extends EventDefinition>(
|
export function once<Definition extends BusEvent.Definition>(
|
||||||
def: Definition,
|
def: Definition,
|
||||||
callback: (event: {
|
callback: (event: {
|
||||||
type: Definition["type"]
|
type: Definition["type"]
|
||||||
|
|||||||
@@ -4,21 +4,73 @@ import { UI } from "../ui"
|
|||||||
import { Global } from "../../global"
|
import { Global } from "../../global"
|
||||||
import { Agent } from "../../agent/agent"
|
import { Agent } from "../../agent/agent"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
import fs from "fs/promises"
|
||||||
import matter from "gray-matter"
|
import matter from "gray-matter"
|
||||||
import { Instance } from "../../project/instance"
|
import { Instance } from "../../project/instance"
|
||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
|
import type { Argv } from "yargs"
|
||||||
|
|
||||||
|
type AgentMode = "all" | "primary" | "subagent"
|
||||||
|
|
||||||
|
const AVAILABLE_TOOLS = [
|
||||||
|
"bash",
|
||||||
|
"read",
|
||||||
|
"write",
|
||||||
|
"edit",
|
||||||
|
"list",
|
||||||
|
"glob",
|
||||||
|
"grep",
|
||||||
|
"webfetch",
|
||||||
|
"task",
|
||||||
|
"todowrite",
|
||||||
|
"todoread",
|
||||||
|
]
|
||||||
|
|
||||||
const AgentCreateCommand = cmd({
|
const AgentCreateCommand = cmd({
|
||||||
command: "create",
|
command: "create",
|
||||||
describe: "create a new agent",
|
describe: "create a new agent",
|
||||||
async handler() {
|
builder: (yargs: Argv) =>
|
||||||
|
yargs
|
||||||
|
.option("path", {
|
||||||
|
type: "string",
|
||||||
|
describe: "directory path to generate the agent file",
|
||||||
|
})
|
||||||
|
.option("description", {
|
||||||
|
type: "string",
|
||||||
|
describe: "what the agent should do",
|
||||||
|
})
|
||||||
|
.option("mode", {
|
||||||
|
type: "string",
|
||||||
|
describe: "agent mode",
|
||||||
|
choices: ["all", "primary", "subagent"] as const,
|
||||||
|
})
|
||||||
|
.option("tools", {
|
||||||
|
type: "string",
|
||||||
|
describe: `comma-separated list of tools to enable (default: all). Available: "${AVAILABLE_TOOLS.join(", ")}"`,
|
||||||
|
}),
|
||||||
|
async handler(args) {
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: process.cwd(),
|
directory: process.cwd(),
|
||||||
async fn() {
|
async fn() {
|
||||||
|
const cliPath = args.path
|
||||||
|
const cliDescription = args.description
|
||||||
|
const cliMode = args.mode as AgentMode | undefined
|
||||||
|
const cliTools = args.tools
|
||||||
|
|
||||||
|
const isFullyNonInteractive = cliPath && cliDescription && cliMode && cliTools !== undefined
|
||||||
|
|
||||||
|
if (!isFullyNonInteractive) {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
prompts.intro("Create agent")
|
prompts.intro("Create agent")
|
||||||
|
}
|
||||||
|
|
||||||
const project = Instance.project
|
const project = Instance.project
|
||||||
|
|
||||||
|
// Determine scope/path
|
||||||
|
let targetPath: string
|
||||||
|
if (cliPath) {
|
||||||
|
targetPath = path.join(cliPath, "agent")
|
||||||
|
} else {
|
||||||
let scope: "global" | "project" = "global"
|
let scope: "global" | "project" = "global"
|
||||||
if (project.vcs === "git") {
|
if (project.vcs === "git") {
|
||||||
const scopeResult = await prompts.select({
|
const scopeResult = await prompts.select({
|
||||||
@@ -39,47 +91,58 @@ const AgentCreateCommand = cmd({
|
|||||||
if (prompts.isCancel(scopeResult)) throw new UI.CancelledError()
|
if (prompts.isCancel(scopeResult)) throw new UI.CancelledError()
|
||||||
scope = scopeResult
|
scope = scopeResult
|
||||||
}
|
}
|
||||||
|
targetPath = path.join(
|
||||||
|
scope === "global" ? Global.Path.config : path.join(Instance.worktree, ".opencode"),
|
||||||
|
"agent",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get description
|
||||||
|
let description: string
|
||||||
|
if (cliDescription) {
|
||||||
|
description = cliDescription
|
||||||
|
} else {
|
||||||
const query = await prompts.text({
|
const query = await prompts.text({
|
||||||
message: "Description",
|
message: "Description",
|
||||||
placeholder: "What should this agent do?",
|
placeholder: "What should this agent do?",
|
||||||
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
|
||||||
})
|
})
|
||||||
if (prompts.isCancel(query)) throw new UI.CancelledError()
|
if (prompts.isCancel(query)) throw new UI.CancelledError()
|
||||||
|
description = query
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate agent
|
||||||
const spinner = prompts.spinner()
|
const spinner = prompts.spinner()
|
||||||
|
|
||||||
spinner.start("Generating agent configuration...")
|
spinner.start("Generating agent configuration...")
|
||||||
const generated = await Agent.generate({ description: query }).catch((error) => {
|
const generated = await Agent.generate({ description }).catch((error) => {
|
||||||
spinner.stop(`LLM failed to generate agent: ${error.message}`, 1)
|
spinner.stop(`LLM failed to generate agent: ${error.message}`, 1)
|
||||||
|
if (isFullyNonInteractive) process.exit(1)
|
||||||
throw new UI.CancelledError()
|
throw new UI.CancelledError()
|
||||||
})
|
})
|
||||||
spinner.stop(`Agent ${generated.identifier} generated`)
|
spinner.stop(`Agent ${generated.identifier} generated`)
|
||||||
|
|
||||||
const availableTools = [
|
// Select tools
|
||||||
"bash",
|
let selectedTools: string[]
|
||||||
"read",
|
if (cliTools !== undefined) {
|
||||||
"write",
|
selectedTools = cliTools ? cliTools.split(",").map((t) => t.trim()) : AVAILABLE_TOOLS
|
||||||
"edit",
|
} else {
|
||||||
"list",
|
const result = await prompts.multiselect({
|
||||||
"glob",
|
|
||||||
"grep",
|
|
||||||
"webfetch",
|
|
||||||
"task",
|
|
||||||
"todowrite",
|
|
||||||
"todoread",
|
|
||||||
]
|
|
||||||
|
|
||||||
const selectedTools = await prompts.multiselect({
|
|
||||||
message: "Select tools to enable",
|
message: "Select tools to enable",
|
||||||
options: availableTools.map((tool) => ({
|
options: AVAILABLE_TOOLS.map((tool) => ({
|
||||||
label: tool,
|
label: tool,
|
||||||
value: tool,
|
value: tool,
|
||||||
})),
|
})),
|
||||||
initialValues: availableTools,
|
initialValues: AVAILABLE_TOOLS,
|
||||||
})
|
})
|
||||||
if (prompts.isCancel(selectedTools)) throw new UI.CancelledError()
|
if (prompts.isCancel(result)) throw new UI.CancelledError()
|
||||||
|
selectedTools = result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get mode
|
||||||
|
let mode: AgentMode
|
||||||
|
if (cliMode) {
|
||||||
|
mode = cliMode
|
||||||
|
} else {
|
||||||
const modeResult = await prompts.select({
|
const modeResult = await prompts.select({
|
||||||
message: "Agent mode",
|
message: "Agent mode",
|
||||||
options: [
|
options: [
|
||||||
@@ -99,36 +162,57 @@ const AgentCreateCommand = cmd({
|
|||||||
hint: "Can be used as a subagent by other agents",
|
hint: "Can be used as a subagent by other agents",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
initialValue: "all",
|
initialValue: "all" as const,
|
||||||
})
|
})
|
||||||
if (prompts.isCancel(modeResult)) throw new UI.CancelledError()
|
if (prompts.isCancel(modeResult)) throw new UI.CancelledError()
|
||||||
|
mode = modeResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build tools config
|
||||||
const tools: Record<string, boolean> = {}
|
const tools: Record<string, boolean> = {}
|
||||||
for (const tool of availableTools) {
|
for (const tool of AVAILABLE_TOOLS) {
|
||||||
if (!selectedTools.includes(tool)) {
|
if (!selectedTools.includes(tool)) {
|
||||||
tools[tool] = false
|
tools[tool] = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const frontmatter: any = {
|
// Build frontmatter
|
||||||
|
const frontmatter: {
|
||||||
|
description: string
|
||||||
|
mode: AgentMode
|
||||||
|
tools?: Record<string, boolean>
|
||||||
|
} = {
|
||||||
description: generated.whenToUse,
|
description: generated.whenToUse,
|
||||||
mode: modeResult,
|
mode,
|
||||||
}
|
}
|
||||||
if (Object.keys(tools).length > 0) {
|
if (Object.keys(tools).length > 0) {
|
||||||
frontmatter.tools = tools
|
frontmatter.tools = tools
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write file
|
||||||
const content = matter.stringify(generated.systemPrompt, frontmatter)
|
const content = matter.stringify(generated.systemPrompt, frontmatter)
|
||||||
const filePath = path.join(
|
const filePath = path.join(targetPath, `${generated.identifier}.md`)
|
||||||
scope === "global" ? Global.Path.config : path.join(Instance.worktree, ".opencode"),
|
|
||||||
`agent`,
|
await fs.mkdir(targetPath, { recursive: true })
|
||||||
`${generated.identifier}.md`,
|
|
||||||
)
|
const file = Bun.file(filePath)
|
||||||
|
if (await file.exists()) {
|
||||||
|
if (isFullyNonInteractive) {
|
||||||
|
console.error(`Error: Agent file already exists: ${filePath}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
prompts.log.error(`Agent file already exists: ${filePath}`)
|
||||||
|
throw new UI.CancelledError()
|
||||||
|
}
|
||||||
|
|
||||||
await Bun.write(filePath, content)
|
await Bun.write(filePath, content)
|
||||||
|
|
||||||
|
if (isFullyNonInteractive) {
|
||||||
|
console.log(filePath)
|
||||||
|
} else {
|
||||||
prompts.log.success(`Agent created: ${filePath}`)
|
prompts.log.success(`Agent created: ${filePath}`)
|
||||||
prompts.outro("Done")
|
prompts.outro("Done")
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -403,12 +403,12 @@ export const GithubRunCommand = cmd({
|
|||||||
let appToken: string
|
let appToken: string
|
||||||
let octoRest: Octokit
|
let octoRest: Octokit
|
||||||
let octoGraph: typeof graphql
|
let octoGraph: typeof graphql
|
||||||
let commentId: number
|
|
||||||
let gitConfig: string
|
let gitConfig: string
|
||||||
let session: { id: string; title: string; version: string }
|
let session: { id: string; title: string; version: string }
|
||||||
let shareId: string | undefined
|
let shareId: string | undefined
|
||||||
let exitCode = 0
|
let exitCode = 0
|
||||||
type PromptFiles = Awaited<ReturnType<typeof getUserPrompt>>["promptFiles"]
|
type PromptFiles = Awaited<ReturnType<typeof getUserPrompt>>["promptFiles"]
|
||||||
|
const triggerCommentId = payload.comment.id
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const actionToken = isMock ? args.token! : await getOidcToken()
|
const actionToken = isMock ? args.token! : await getOidcToken()
|
||||||
@@ -422,8 +422,7 @@ export const GithubRunCommand = cmd({
|
|||||||
await configureGit(appToken)
|
await configureGit(appToken)
|
||||||
await assertPermissions()
|
await assertPermissions()
|
||||||
|
|
||||||
const comment = await createComment()
|
await addReaction("eyes")
|
||||||
commentId = comment.data.id
|
|
||||||
|
|
||||||
// Setup opencode session
|
// Setup opencode session
|
||||||
const repoData = await fetchRepo()
|
const repoData = await fetchRepo()
|
||||||
@@ -455,7 +454,8 @@ export const GithubRunCommand = cmd({
|
|||||||
await pushToLocalBranch(summary, uncommittedChanges)
|
await pushToLocalBranch(summary, uncommittedChanges)
|
||||||
}
|
}
|
||||||
const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${shareBaseUrl}/s/${shareId}`))
|
const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${shareBaseUrl}/s/${shareId}`))
|
||||||
await updateComment(`${response}${footer({ image: !hasShared })}`)
|
await createComment(`${response}${footer({ image: !hasShared })}`)
|
||||||
|
await removeReaction()
|
||||||
}
|
}
|
||||||
// Fork PR
|
// Fork PR
|
||||||
else {
|
else {
|
||||||
@@ -469,7 +469,8 @@ export const GithubRunCommand = cmd({
|
|||||||
await pushToForkBranch(summary, prData, uncommittedChanges)
|
await pushToForkBranch(summary, prData, uncommittedChanges)
|
||||||
}
|
}
|
||||||
const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${shareBaseUrl}/s/${shareId}`))
|
const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${shareBaseUrl}/s/${shareId}`))
|
||||||
await updateComment(`${response}${footer({ image: !hasShared })}`)
|
await createComment(`${response}${footer({ image: !hasShared })}`)
|
||||||
|
await removeReaction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Issue
|
// Issue
|
||||||
@@ -489,9 +490,11 @@ export const GithubRunCommand = cmd({
|
|||||||
summary,
|
summary,
|
||||||
`${response}\n\nCloses #${issueId}${footer({ image: true })}`,
|
`${response}\n\nCloses #${issueId}${footer({ image: true })}`,
|
||||||
)
|
)
|
||||||
await updateComment(`Created PR #${pr}${footer({ image: true })}`)
|
await createComment(`Created PR #${pr}${footer({ image: true })}`)
|
||||||
|
await removeReaction()
|
||||||
} else {
|
} else {
|
||||||
await updateComment(`${response}${footer({ image: true })}`)
|
await createComment(`${response}${footer({ image: true })}`)
|
||||||
|
await removeReaction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -503,7 +506,8 @@ export const GithubRunCommand = cmd({
|
|||||||
} else if (e instanceof Error) {
|
} else if (e instanceof Error) {
|
||||||
msg = e.message
|
msg = e.message
|
||||||
}
|
}
|
||||||
await updateComment(`${msg}${footer()}`)
|
await createComment(`${msg}${footer()}`)
|
||||||
|
await removeReaction()
|
||||||
core.setFailed(msg)
|
core.setFailed(msg)
|
||||||
// Also output the clean error message for the action to capture
|
// Also output the clean error message for the action to capture
|
||||||
//core.setOutput("prepare_error", e.message);
|
//core.setOutput("prepare_error", e.message);
|
||||||
@@ -931,24 +935,41 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
|
|||||||
if (!["admin", "write"].includes(permission)) throw new Error(`User ${actor} does not have write permissions`)
|
if (!["admin", "write"].includes(permission)) throw new Error(`User ${actor} does not have write permissions`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createComment() {
|
async function addReaction(reaction: "eyes") {
|
||||||
|
console.log("Adding reaction...")
|
||||||
|
return await octoRest.rest.reactions.createForIssueComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
comment_id: triggerCommentId,
|
||||||
|
content: reaction,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeReaction() {
|
||||||
|
console.log("Removing reaction...")
|
||||||
|
const reactions = await octoRest.rest.reactions.listForIssueComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
comment_id: triggerCommentId,
|
||||||
|
})
|
||||||
|
|
||||||
|
const eyesReaction = reactions.data.find((r) => r.content === "eyes")
|
||||||
|
if (!eyesReaction) return
|
||||||
|
|
||||||
|
await octoRest.rest.reactions.deleteForIssueComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
comment_id: triggerCommentId,
|
||||||
|
reaction_id: eyesReaction.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createComment(body: string) {
|
||||||
console.log("Creating comment...")
|
console.log("Creating comment...")
|
||||||
return await octoRest.rest.issues.createComment({
|
return await octoRest.rest.issues.createComment({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
issue_number: issueId,
|
issue_number: issueId,
|
||||||
body: `[Working...](${runUrl})`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateComment(body: string) {
|
|
||||||
if (!commentId) return
|
|
||||||
|
|
||||||
console.log("Updating comment...")
|
|
||||||
return await octoRest.rest.issues.updateComment({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
comment_id: commentId,
|
|
||||||
body,
|
body,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1029,7 +1050,7 @@ query($owner: String!, $repo: String!, $number: Int!) {
|
|||||||
const comments = (issue.comments?.nodes || [])
|
const comments = (issue.comments?.nodes || [])
|
||||||
.filter((c) => {
|
.filter((c) => {
|
||||||
const id = parseInt(c.databaseId)
|
const id = parseInt(c.databaseId)
|
||||||
return id !== commentId && id !== payload.comment.id
|
return id !== payload.comment.id
|
||||||
})
|
})
|
||||||
.map((c) => ` - ${c.author.login} at ${c.createdAt}: ${c.body}`)
|
.map((c) => ` - ${c.author.login} at ${c.createdAt}: ${c.body}`)
|
||||||
|
|
||||||
@@ -1148,7 +1169,7 @@ query($owner: String!, $repo: String!, $number: Int!) {
|
|||||||
const comments = (pr.comments?.nodes || [])
|
const comments = (pr.comments?.nodes || [])
|
||||||
.filter((c) => {
|
.filter((c) => {
|
||||||
const id = parseInt(c.databaseId)
|
const id = parseInt(c.databaseId)
|
||||||
return id !== commentId && id !== payload.comment.id
|
return id !== payload.comment.id
|
||||||
})
|
})
|
||||||
.map((c) => `- ${c.author.login} at ${c.createdAt}: ${c.body}`)
|
.map((c) => `- ${c.author.login} at ${c.createdAt}: ${c.body}`)
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export function tui(input: { url: string; args: Args; onExit?: () => Promise<voi
|
|||||||
targetFps: 60,
|
targetFps: 60,
|
||||||
gatherStats: false,
|
gatherStats: false,
|
||||||
exitOnCtrlC: false,
|
exitOnCtrlC: false,
|
||||||
useKittyKeyboard: true,
|
useKittyKeyboard: {},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
const content = await Editor.open({ value, renderer })
|
const content = await Editor.open({ value, renderer })
|
||||||
if (!content) return
|
if (!content) return
|
||||||
|
|
||||||
input.setText(content, { history: false })
|
input.setText(content)
|
||||||
|
|
||||||
// Update positions for nonTextParts based on their location in new content
|
// Update positions for nonTextParts based on their location in new content
|
||||||
// Filter out parts whose virtual text was deleted
|
// Filter out parts whose virtual text was deleted
|
||||||
@@ -390,7 +390,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
input.blur()
|
input.blur()
|
||||||
},
|
},
|
||||||
set(prompt) {
|
set(prompt) {
|
||||||
input.setText(prompt.input, { history: false })
|
input.setText(prompt.input)
|
||||||
setStore("prompt", prompt)
|
setStore("prompt", prompt)
|
||||||
restoreExtmarksFromParts(prompt.parts)
|
restoreExtmarksFromParts(prompt.parts)
|
||||||
input.gotoBufferEnd()
|
input.gotoBufferEnd()
|
||||||
@@ -410,6 +410,11 @@ export function Prompt(props: PromptProps) {
|
|||||||
if (props.disabled) return
|
if (props.disabled) return
|
||||||
if (autocomplete.visible) return
|
if (autocomplete.visible) return
|
||||||
if (!store.prompt.input) return
|
if (!store.prompt.input) return
|
||||||
|
const trimmed = store.prompt.input.trim()
|
||||||
|
if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") {
|
||||||
|
exit()
|
||||||
|
return
|
||||||
|
}
|
||||||
const selectedModel = local.model.current()
|
const selectedModel = local.model.current()
|
||||||
if (!selectedModel) {
|
if (!selectedModel) {
|
||||||
promptModelWarning()
|
promptModelWarning()
|
||||||
@@ -683,17 +688,6 @@ export function Prompt(props: PromptProps) {
|
|||||||
setStore("extmarkToPartIndex", new Map())
|
setStore("extmarkToPartIndex", new Map())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (keybind.match("input_forward_delete", e) && store.prompt.input !== "") {
|
|
||||||
const cursorOffset = input.cursorOffset
|
|
||||||
if (cursorOffset < input.plainText.length) {
|
|
||||||
const text = input.plainText
|
|
||||||
const newText = text.slice(0, cursorOffset) + text.slice(cursorOffset + 1)
|
|
||||||
input.setText(newText)
|
|
||||||
input.cursorOffset = cursorOffset
|
|
||||||
}
|
|
||||||
e.preventDefault()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (keybind.match("app_exit", e)) {
|
if (keybind.match("app_exit", e)) {
|
||||||
await exit()
|
await exit()
|
||||||
return
|
return
|
||||||
@@ -720,7 +714,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
const item = history.move(direction, input.plainText)
|
const item = history.move(direction, input.plainText)
|
||||||
|
|
||||||
if (item) {
|
if (item) {
|
||||||
input.setText(item.input, { history: false })
|
input.setText(item.input)
|
||||||
setStore("prompt", item)
|
setStore("prompt", item)
|
||||||
restoreExtmarksFromParts(item.parts)
|
restoreExtmarksFromParts(item.parts)
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -813,10 +807,10 @@ export function Prompt(props: PromptProps) {
|
|||||||
</text>
|
</text>
|
||||||
<Show when={store.mode === "normal"}>
|
<Show when={store.mode === "normal"}>
|
||||||
<box flexDirection="row" gap={1}>
|
<box flexDirection="row" gap={1}>
|
||||||
<text fg={theme.textMuted}>{local.model.parsed().provider}</text>
|
|
||||||
<text flexShrink={0} fg={theme.text}>
|
<text flexShrink={0} fg={theme.text}>
|
||||||
{local.model.parsed().model}
|
{local.model.parsed().model}
|
||||||
</text>
|
</text>
|
||||||
|
<text fg={theme.textMuted}>{local.model.parsed().provider}</text>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"darkAccent": "#FFF7F1",
|
"darkAccent": "#FFF7F1",
|
||||||
"darkRed": "#e06c75",
|
"darkRed": "#e06c75",
|
||||||
"darkOrange": "#EC5B2B",
|
"darkOrange": "#EC5B2B",
|
||||||
"darkGreen": "#7fd88f",
|
"darkBlue": "#6ba1e6",
|
||||||
"darkCyan": "#56b6c2",
|
"darkCyan": "#56b6c2",
|
||||||
"darkYellow": "#e5c07b",
|
"darkYellow": "#e5c07b",
|
||||||
"lightStep1": "#ffffff",
|
"lightStep1": "#ffffff",
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
"lightAccent": "#c94d24",
|
"lightAccent": "#c94d24",
|
||||||
"lightRed": "#d1383d",
|
"lightRed": "#d1383d",
|
||||||
"lightOrange": "#EC5B2B",
|
"lightOrange": "#EC5B2B",
|
||||||
"lightGreen": "#3d9a57",
|
"lightBlue": "#0062d1",
|
||||||
"lightCyan": "#318795",
|
"lightCyan": "#318795",
|
||||||
"lightYellow": "#b0851f"
|
"lightYellow": "#b0851f"
|
||||||
},
|
},
|
||||||
@@ -62,8 +62,8 @@
|
|||||||
"light": "lightOrange"
|
"light": "lightOrange"
|
||||||
},
|
},
|
||||||
"success": {
|
"success": {
|
||||||
"dark": "darkGreen",
|
"dark": "darkBlue",
|
||||||
"light": "lightGreen"
|
"light": "lightBlue"
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
"dark": "darkCyan",
|
"dark": "darkCyan",
|
||||||
@@ -102,8 +102,8 @@
|
|||||||
"light": "lightStep6"
|
"light": "lightStep6"
|
||||||
},
|
},
|
||||||
"diffAdded": {
|
"diffAdded": {
|
||||||
"dark": "#4fd6be",
|
"dark": "#6ba1e6",
|
||||||
"light": "#1e725c"
|
"light": "#0062d1"
|
||||||
},
|
},
|
||||||
"diffRemoved": {
|
"diffRemoved": {
|
||||||
"dark": "#c53b53",
|
"dark": "#c53b53",
|
||||||
@@ -118,16 +118,16 @@
|
|||||||
"light": "#7086b5"
|
"light": "#7086b5"
|
||||||
},
|
},
|
||||||
"diffHighlightAdded": {
|
"diffHighlightAdded": {
|
||||||
"dark": "#b8db87",
|
"dark": "#6ba1e6",
|
||||||
"light": "#4db380"
|
"light": "#0062d1"
|
||||||
},
|
},
|
||||||
"diffHighlightRemoved": {
|
"diffHighlightRemoved": {
|
||||||
"dark": "#e26a75",
|
"dark": "#e26a75",
|
||||||
"light": "#f52a65"
|
"light": "#f52a65"
|
||||||
},
|
},
|
||||||
"diffAddedBg": {
|
"diffAddedBg": {
|
||||||
"dark": "#20303b",
|
"dark": "#1a2a3d",
|
||||||
"light": "#d5e5d5"
|
"light": "#e0edfa"
|
||||||
},
|
},
|
||||||
"diffRemovedBg": {
|
"diffRemovedBg": {
|
||||||
"dark": "#37222c",
|
"dark": "#37222c",
|
||||||
@@ -142,8 +142,8 @@
|
|||||||
"light": "lightStep3"
|
"light": "lightStep3"
|
||||||
},
|
},
|
||||||
"diffAddedLineNumberBg": {
|
"diffAddedLineNumberBg": {
|
||||||
"dark": "#1b2b34",
|
"dark": "#162535",
|
||||||
"light": "#c5d5c5"
|
"light": "#d0e5f5"
|
||||||
},
|
},
|
||||||
"diffRemovedLineNumberBg": {
|
"diffRemovedLineNumberBg": {
|
||||||
"dark": "#2d1f26",
|
"dark": "#2d1f26",
|
||||||
@@ -166,8 +166,8 @@
|
|||||||
"light": "lightCyan"
|
"light": "lightCyan"
|
||||||
},
|
},
|
||||||
"markdownCode": {
|
"markdownCode": {
|
||||||
"dark": "darkGreen",
|
"dark": "darkBlue",
|
||||||
"light": "lightGreen"
|
"light": "lightBlue"
|
||||||
},
|
},
|
||||||
"markdownBlockQuote": {
|
"markdownBlockQuote": {
|
||||||
"dark": "#FFF7F1",
|
"dark": "#FFF7F1",
|
||||||
@@ -222,8 +222,8 @@
|
|||||||
"light": "lightRed"
|
"light": "lightRed"
|
||||||
},
|
},
|
||||||
"syntaxString": {
|
"syntaxString": {
|
||||||
"dark": "darkGreen",
|
"dark": "darkBlue",
|
||||||
"light": "lightGreen"
|
"light": "lightBlue"
|
||||||
},
|
},
|
||||||
"syntaxNumber": {
|
"syntaxNumber": {
|
||||||
"dark": "#FFF7F1",
|
"dark": "#FFF7F1",
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
import { Bus } from "@/bus"
|
import { Bus } from "@/bus"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
|
|
||||||
export const TuiEvent = {
|
export const TuiEvent = {
|
||||||
PromptAppend: Bus.event("tui.prompt.append", z.object({ text: z.string() })),
|
PromptAppend: BusEvent.define("tui.prompt.append", z.object({ text: z.string() })),
|
||||||
CommandExecute: Bus.event(
|
CommandExecute: BusEvent.define(
|
||||||
"tui.command.execute",
|
"tui.command.execute",
|
||||||
z.object({
|
z.object({
|
||||||
command: z.union([
|
command: z.union([
|
||||||
@@ -27,7 +28,7 @@ export const TuiEvent = {
|
|||||||
]),
|
]),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
ToastShow: Bus.event(
|
ToastShow: BusEvent.define(
|
||||||
"tui.toast.show",
|
"tui.toast.show",
|
||||||
z.object({
|
z.object({
|
||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export function Footer() {
|
|||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const mcp = createMemo(() => Object.keys(sync.data.mcp))
|
const mcp = createMemo(() => Object.values(sync.data.mcp).filter((x) => x.status === "connected").length)
|
||||||
const mcpError = createMemo(() => Object.values(sync.data.mcp).some((x) => x.status === "failed"))
|
const mcpError = createMemo(() => Object.values(sync.data.mcp).some((x) => x.status === "failed"))
|
||||||
const lsp = createMemo(() => Object.keys(sync.data.lsp))
|
const lsp = createMemo(() => Object.keys(sync.data.lsp))
|
||||||
const permissions = createMemo(() => {
|
const permissions = createMemo(() => {
|
||||||
@@ -66,7 +66,7 @@ export function Footer() {
|
|||||||
<text fg={theme.text}>
|
<text fg={theme.text}>
|
||||||
<span style={{ fg: theme.success }}>•</span> {lsp().length} LSP
|
<span style={{ fg: theme.success }}>•</span> {lsp().length} LSP
|
||||||
</text>
|
</text>
|
||||||
<Show when={mcp().length}>
|
<Show when={mcp()}>
|
||||||
<text fg={theme.text}>
|
<text fg={theme.text}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={mcpError()}>
|
<Match when={mcpError()}>
|
||||||
@@ -76,7 +76,7 @@ export function Footer() {
|
|||||||
<span style={{ fg: theme.success }}>⊙ </span>
|
<span style={{ fg: theme.success }}>⊙ </span>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
{mcp().length} MCP
|
{mcp()} MCP
|
||||||
</text>
|
</text>
|
||||||
</Show>
|
</Show>
|
||||||
<text fg={theme.textMuted}>/status</text>
|
<text fg={theme.textMuted}>/status</text>
|
||||||
|
|||||||
@@ -1057,6 +1057,7 @@ function UserMessage(props: {
|
|||||||
</Show>
|
</Show>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<span> </span>
|
||||||
<span style={{ bg: theme.accent, fg: theme.backgroundPanel, bold: true }}> QUEUED </span>
|
<span style={{ bg: theme.accent, fg: theme.backgroundPanel, bold: true }}> QUEUED </span>
|
||||||
</Show>
|
</Show>
|
||||||
</text>
|
</text>
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
|
import { Bus } from "@/bus"
|
||||||
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Config } from "../config/config"
|
import { Config } from "../config/config"
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
import { Bus } from "../bus"
|
|
||||||
import { Identifier } from "../id/id"
|
import { Identifier } from "../id/id"
|
||||||
import PROMPT_INITIALIZE from "./template/initialize.txt"
|
import PROMPT_INITIALIZE from "./template/initialize.txt"
|
||||||
import PROMPT_REVIEW from "./template/review.txt"
|
import PROMPT_REVIEW from "./template/review.txt"
|
||||||
|
|
||||||
export namespace Command {
|
export namespace Command {
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Executed: Bus.event(
|
Executed: BusEvent.define(
|
||||||
"command.executed",
|
"command.executed",
|
||||||
z.object({
|
z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
|||||||
@@ -464,7 +464,6 @@ export namespace Config {
|
|||||||
agent_cycle: z.string().optional().default("tab").describe("Next agent"),
|
agent_cycle: z.string().optional().default("tab").describe("Next agent"),
|
||||||
agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
|
agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
|
||||||
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
|
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
|
||||||
input_forward_delete: z.string().optional().default("ctrl+d").describe("Forward delete"),
|
|
||||||
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
|
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
|
||||||
input_submit: z.string().optional().default("return").describe("Submit input"),
|
input_submit: z.string().optional().default("return").describe("Submit input"),
|
||||||
input_newline: z.string().optional().default("shift+return,ctrl+j").describe("Insert newline in input"),
|
input_newline: z.string().optional().default("shift+return,ctrl+j").describe("Insert newline in input"),
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
import path from "path"
|
|
||||||
import { Global } from "../global"
|
|
||||||
import fs from "fs/promises"
|
|
||||||
import z from "zod"
|
|
||||||
import { NamedError } from "@opencode-ai/util/error"
|
|
||||||
import { lazy } from "../util/lazy"
|
|
||||||
import { Log } from "../util/log"
|
|
||||||
import { ZipReader, BlobReader, BlobWriter } from "@zip.js/zip.js"
|
|
||||||
|
|
||||||
export namespace Fzf {
|
|
||||||
const log = Log.create({ service: "fzf" })
|
|
||||||
|
|
||||||
const VERSION = "0.62.0"
|
|
||||||
const PLATFORM = {
|
|
||||||
darwin: { extension: "tar.gz" },
|
|
||||||
linux: { extension: "tar.gz" },
|
|
||||||
win32: { extension: "zip" },
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export const ExtractionFailedError = NamedError.create(
|
|
||||||
"FzfExtractionFailedError",
|
|
||||||
z.object({
|
|
||||||
filepath: z.string(),
|
|
||||||
stderr: z.string(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const UnsupportedPlatformError = NamedError.create(
|
|
||||||
"FzfUnsupportedPlatformError",
|
|
||||||
z.object({
|
|
||||||
platform: z.string(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const DownloadFailedError = NamedError.create(
|
|
||||||
"FzfDownloadFailedError",
|
|
||||||
z.object({
|
|
||||||
url: z.string(),
|
|
||||||
status: z.number(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const state = lazy(async () => {
|
|
||||||
let filepath = Bun.which("fzf")
|
|
||||||
if (filepath) {
|
|
||||||
log.info("found", { filepath })
|
|
||||||
return { filepath }
|
|
||||||
}
|
|
||||||
filepath = path.join(Global.Path.bin, "fzf" + (process.platform === "win32" ? ".exe" : ""))
|
|
||||||
|
|
||||||
const file = Bun.file(filepath)
|
|
||||||
if (!(await file.exists())) {
|
|
||||||
const archMap = { x64: "amd64", arm64: "arm64" } as const
|
|
||||||
const arch = archMap[process.arch as keyof typeof archMap] ?? "amd64"
|
|
||||||
|
|
||||||
const config = PLATFORM[process.platform as keyof typeof PLATFORM]
|
|
||||||
if (!config) throw new UnsupportedPlatformError({ platform: process.platform })
|
|
||||||
|
|
||||||
const version = VERSION
|
|
||||||
const platformName = process.platform === "win32" ? "windows" : process.platform
|
|
||||||
const filename = `fzf-${version}-${platformName}_${arch}.${config.extension}`
|
|
||||||
const url = `https://github.com/junegunn/fzf/releases/download/v${version}/${filename}`
|
|
||||||
|
|
||||||
const response = await fetch(url)
|
|
||||||
if (!response.ok) throw new DownloadFailedError({ url, status: response.status })
|
|
||||||
|
|
||||||
const buffer = await response.arrayBuffer()
|
|
||||||
const archivePath = path.join(Global.Path.bin, filename)
|
|
||||||
await Bun.write(archivePath, buffer)
|
|
||||||
if (config.extension === "tar.gz") {
|
|
||||||
const proc = Bun.spawn(["tar", "-xzf", archivePath, "fzf"], {
|
|
||||||
cwd: Global.Path.bin,
|
|
||||||
stderr: "pipe",
|
|
||||||
stdout: "pipe",
|
|
||||||
})
|
|
||||||
await proc.exited
|
|
||||||
if (proc.exitCode !== 0)
|
|
||||||
throw new ExtractionFailedError({
|
|
||||||
filepath,
|
|
||||||
stderr: await Bun.readableStreamToText(proc.stderr),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (config.extension === "zip") {
|
|
||||||
const zipFileReader = new ZipReader(new BlobReader(new Blob([await Bun.file(archivePath).arrayBuffer()])))
|
|
||||||
const entries = await zipFileReader.getEntries()
|
|
||||||
let fzfEntry: any
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (entry.filename === "fzf.exe") {
|
|
||||||
fzfEntry = entry
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fzfEntry) {
|
|
||||||
throw new ExtractionFailedError({
|
|
||||||
filepath: archivePath,
|
|
||||||
stderr: "fzf.exe not found in zip archive",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const fzfBlob = await fzfEntry.getData(new BlobWriter())
|
|
||||||
if (!fzfBlob) {
|
|
||||||
throw new ExtractionFailedError({
|
|
||||||
filepath: archivePath,
|
|
||||||
stderr: "Failed to extract fzf.exe from zip archive",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
await Bun.write(filepath, await fzfBlob.arrayBuffer())
|
|
||||||
await zipFileReader.close()
|
|
||||||
}
|
|
||||||
await fs.unlink(archivePath)
|
|
||||||
if (process.platform !== "win32") await fs.chmod(filepath, 0o755)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
filepath,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export async function filepath() {
|
|
||||||
const { filepath } = await state()
|
|
||||||
return filepath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
|
import { Bus } from "@/bus"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Bus } from "../bus"
|
|
||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
import type { BunFile } from "bun"
|
import type { BunFile } from "bun"
|
||||||
import { formatPatch, structuredPatch } from "diff"
|
import { formatPatch, structuredPatch } from "diff"
|
||||||
@@ -111,7 +112,7 @@ export namespace File {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Edited: Bus.event(
|
Edited: BusEvent.define(
|
||||||
"file.edited",
|
"file.edited",
|
||||||
z.object({
|
z.object({
|
||||||
file: z.string(),
|
file: z.string(),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
|
import { Bus } from "@/bus"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Bus } from "../bus"
|
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { FileIgnore } from "./ignore"
|
import { FileIgnore } from "./ignore"
|
||||||
@@ -8,6 +9,7 @@ import { Config } from "../config/config"
|
|||||||
import { createWrapper } from "@parcel/watcher/wrapper"
|
import { createWrapper } from "@parcel/watcher/wrapper"
|
||||||
import { lazy } from "@/util/lazy"
|
import { lazy } from "@/util/lazy"
|
||||||
import type ParcelWatcher from "@parcel/watcher"
|
import type ParcelWatcher from "@parcel/watcher"
|
||||||
|
import { $ } from "bun"
|
||||||
|
|
||||||
declare const OPENCODE_LIBC: string | undefined
|
declare const OPENCODE_LIBC: string | undefined
|
||||||
|
|
||||||
@@ -15,7 +17,7 @@ export namespace FileWatcher {
|
|||||||
const log = Log.create({ service: "file.watcher" })
|
const log = Log.create({ service: "file.watcher" })
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Updated: Bus.event(
|
Updated: BusEvent.define(
|
||||||
"file.watcher.updated",
|
"file.watcher.updated",
|
||||||
z.object({
|
z.object({
|
||||||
file: z.string(),
|
file: z.string(),
|
||||||
@@ -65,7 +67,7 @@ export namespace FileWatcher {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const vcsDir = Instance.project.vcsDir
|
const vcsDir = await $`git rev-parse --git-dir`.quiet().nothrow().cwd(Instance.worktree).text()
|
||||||
if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) {
|
if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) {
|
||||||
subs.push(
|
subs.push(
|
||||||
await watcher().subscribe(vcsDir, subscribe, {
|
await watcher().subscribe(vcsDir, subscribe, {
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ export namespace Flag {
|
|||||||
export const OPENCODE_ENABLE_EXPERIMENTAL_MODELS = truthy("OPENCODE_ENABLE_EXPERIMENTAL_MODELS")
|
export const OPENCODE_ENABLE_EXPERIMENTAL_MODELS = truthy("OPENCODE_ENABLE_EXPERIMENTAL_MODELS")
|
||||||
export const OPENCODE_DISABLE_AUTOCOMPACT = truthy("OPENCODE_DISABLE_AUTOCOMPACT")
|
export const OPENCODE_DISABLE_AUTOCOMPACT = truthy("OPENCODE_DISABLE_AUTOCOMPACT")
|
||||||
export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
|
export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
|
||||||
export const OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH =
|
|
||||||
process.env["OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH"]
|
|
||||||
|
|
||||||
// Experimental
|
// Experimental
|
||||||
export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL")
|
export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL")
|
||||||
@@ -20,9 +18,18 @@ export namespace Flag {
|
|||||||
export const OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT = truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT")
|
export const OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT = truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT")
|
||||||
export const OPENCODE_ENABLE_EXA =
|
export const OPENCODE_ENABLE_EXA =
|
||||||
truthy("OPENCODE_ENABLE_EXA") || OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_EXA")
|
truthy("OPENCODE_ENABLE_EXA") || OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_EXA")
|
||||||
|
export const OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH = number("OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH")
|
||||||
|
export const OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS = number("OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS")
|
||||||
|
|
||||||
function truthy(key: string) {
|
function truthy(key: string) {
|
||||||
const value = process.env[key]?.toLowerCase()
|
const value = process.env[key]?.toLowerCase()
|
||||||
return value === "true" || value === "1"
|
return value === "true" || value === "1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function number(key: string) {
|
||||||
|
const value = process.env[key]
|
||||||
|
if (!value) return undefined
|
||||||
|
const parsed = Number(value)
|
||||||
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
|
import { Bus } from "@/bus"
|
||||||
import { spawn } from "bun"
|
import { spawn } from "bun"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { NamedError } from "@opencode-ai/util/error"
|
import { NamedError } from "@opencode-ai/util/error"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { Bus } from "../bus"
|
|
||||||
|
|
||||||
const SUPPORTED_IDES = [
|
const SUPPORTED_IDES = [
|
||||||
{ name: "Windsurf" as const, cmd: "windsurf" },
|
{ name: "Windsurf" as const, cmd: "windsurf" },
|
||||||
@@ -16,7 +17,7 @@ export namespace Ide {
|
|||||||
const log = Log.create({ service: "ide" })
|
const log = Log.create({ service: "ide" })
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Installed: Bus.event(
|
Installed: BusEvent.define(
|
||||||
"ide.installed",
|
"ide.installed",
|
||||||
z.object({
|
z.object({
|
||||||
ide: z.string(),
|
ide: z.string(),
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
|
import { Bus } from "@/bus"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { NamedError } from "@opencode-ai/util/error"
|
import { NamedError } from "@opencode-ai/util/error"
|
||||||
import { Bus } from "../bus"
|
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { iife } from "@/util/iife"
|
import { iife } from "@/util/iife"
|
||||||
|
|
||||||
@@ -17,13 +18,13 @@ export namespace Installation {
|
|||||||
export type Method = Awaited<ReturnType<typeof method>>
|
export type Method = Awaited<ReturnType<typeof method>>
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Updated: Bus.event(
|
Updated: BusEvent.define(
|
||||||
"installation.updated",
|
"installation.updated",
|
||||||
z.object({
|
z.object({
|
||||||
version: z.string(),
|
version: z.string(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
UpdateAvailable: Bus.event(
|
UpdateAvailable: BusEvent.define(
|
||||||
"installation.update-available",
|
"installation.update-available",
|
||||||
z.object({
|
z.object({
|
||||||
version: z.string(),
|
version: z.string(),
|
||||||
@@ -183,7 +184,8 @@ export namespace Installation {
|
|||||||
return reg.endsWith("/") ? reg.slice(0, -1) : reg
|
return reg.endsWith("/") ? reg.slice(0, -1) : reg
|
||||||
})
|
})
|
||||||
const [major] = VERSION.split(".").map((x) => Number(x))
|
const [major] = VERSION.split(".").map((x) => Number(x))
|
||||||
const channel = CHANNEL === "latest" ? `latest-${major}` : CHANNEL
|
// const channel = CHANNEL === "latest" ? `latest-${major}` : CHANNEL
|
||||||
|
const channel = CHANNEL
|
||||||
return fetch(`${registry}/opencode-ai/${channel}`)
|
return fetch(`${registry}/opencode-ai/${channel}`)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (!res.ok) throw new Error(res.statusText)
|
if (!res.ok) throw new Error(res.statusText)
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
|
import { Bus } from "@/bus"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node"
|
import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node"
|
||||||
import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"
|
import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { LANGUAGE_EXTENSIONS } from "./language"
|
import { LANGUAGE_EXTENSIONS } from "./language"
|
||||||
import { Bus } from "../bus"
|
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import type { LSPServer } from "./server"
|
import type { LSPServer } from "./server"
|
||||||
import { NamedError } from "@opencode-ai/util/error"
|
import { NamedError } from "@opencode-ai/util/error"
|
||||||
@@ -25,7 +26,7 @@ export namespace LSPClient {
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Diagnostics: Bus.event(
|
Diagnostics: BusEvent.define(
|
||||||
"lsp.client.diagnostics",
|
"lsp.client.diagnostics",
|
||||||
z.object({
|
z.object({
|
||||||
serverID: z.string(),
|
serverID: z.string(),
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
|
import { Bus } from "@/bus"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { LSPClient } from "./client"
|
import { LSPClient } from "./client"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
@@ -6,13 +8,12 @@ import z from "zod"
|
|||||||
import { Config } from "../config/config"
|
import { Config } from "../config/config"
|
||||||
import { spawn } from "child_process"
|
import { spawn } from "child_process"
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
import { Bus } from "../bus"
|
|
||||||
|
|
||||||
export namespace LSP {
|
export namespace LSP {
|
||||||
const log = Log.create({ service: "lsp" })
|
const log = Log.create({ service: "lsp" })
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Updated: Bus.event("lsp.updated", z.object({})),
|
Updated: BusEvent.define("lsp.updated", z.object({})),
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Range = z
|
export const Range = z
|
||||||
|
|||||||
@@ -209,6 +209,68 @@ export namespace LSPServer {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const Biome: Info = {
|
||||||
|
id: "biome",
|
||||||
|
root: NearestRoot([
|
||||||
|
"biome.json",
|
||||||
|
"biome.jsonc",
|
||||||
|
"package-lock.json",
|
||||||
|
"bun.lockb",
|
||||||
|
"bun.lock",
|
||||||
|
"pnpm-lock.yaml",
|
||||||
|
"yarn.lock",
|
||||||
|
]),
|
||||||
|
extensions: [
|
||||||
|
".ts",
|
||||||
|
".tsx",
|
||||||
|
".js",
|
||||||
|
".jsx",
|
||||||
|
".mjs",
|
||||||
|
".cjs",
|
||||||
|
".mts",
|
||||||
|
".cts",
|
||||||
|
".json",
|
||||||
|
".jsonc",
|
||||||
|
".vue",
|
||||||
|
".astro",
|
||||||
|
".svelte",
|
||||||
|
".css",
|
||||||
|
".graphql",
|
||||||
|
".gql",
|
||||||
|
".html",
|
||||||
|
],
|
||||||
|
async spawn(root) {
|
||||||
|
const localBin = path.join(root, "node_modules", ".bin", "biome")
|
||||||
|
let bin: string | undefined
|
||||||
|
if (await Bun.file(localBin).exists()) bin = localBin
|
||||||
|
if (!bin) {
|
||||||
|
const found = Bun.which("biome")
|
||||||
|
if (found) bin = found
|
||||||
|
}
|
||||||
|
|
||||||
|
let args = ["lsp-proxy", "--stdio"]
|
||||||
|
|
||||||
|
if (!bin) {
|
||||||
|
const resolved = await Bun.resolve("biome", root).catch(() => undefined)
|
||||||
|
if (!resolved) return
|
||||||
|
bin = BunProc.which()
|
||||||
|
args = ["x", "biome", "lsp-proxy", "--stdio"]
|
||||||
|
}
|
||||||
|
|
||||||
|
const proc = spawn(bin, args, {
|
||||||
|
cwd: root,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
BUN_BE_BUN: "1",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
process: proc,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
export const Gopls: Info = {
|
export const Gopls: Info = {
|
||||||
id: "gopls",
|
id: "gopls",
|
||||||
root: async (file) => {
|
root: async (file) => {
|
||||||
|
|||||||
@@ -189,14 +189,14 @@ export namespace MCP {
|
|||||||
name: "StreamableHTTP",
|
name: "StreamableHTTP",
|
||||||
transport: new StreamableHTTPClientTransport(new URL(mcp.url), {
|
transport: new StreamableHTTPClientTransport(new URL(mcp.url), {
|
||||||
authProvider,
|
authProvider,
|
||||||
requestInit: oauthDisabled && mcp.headers ? { headers: mcp.headers } : undefined,
|
requestInit: mcp.headers ? { headers: mcp.headers } : undefined,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "SSE",
|
name: "SSE",
|
||||||
transport: new SSEClientTransport(new URL(mcp.url), {
|
transport: new SSEClientTransport(new URL(mcp.url), {
|
||||||
authProvider,
|
authProvider,
|
||||||
requestInit: oauthDisabled && mcp.headers ? { headers: mcp.headers } : undefined,
|
requestInit: mcp.headers ? { headers: mcp.headers } : undefined,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
|
import { Bus } from "@/bus"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Bus } from "../bus"
|
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { Identifier } from "../id/id"
|
import { Identifier } from "../id/id"
|
||||||
import { Plugin } from "../plugin"
|
import { Plugin } from "../plugin"
|
||||||
@@ -38,8 +39,8 @@ export namespace Permission {
|
|||||||
export type Info = z.infer<typeof Info>
|
export type Info = z.infer<typeof Info>
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Updated: Bus.event("permission.updated", Info),
|
Updated: BusEvent.define("permission.updated", Info),
|
||||||
Replied: Bus.event(
|
Replied: BusEvent.define(
|
||||||
"permission.replied",
|
"permission.replied",
|
||||||
z.object({
|
z.object({
|
||||||
sessionID: z.string(),
|
sessionID: z.string(),
|
||||||
|
|||||||
@@ -61,14 +61,10 @@ export namespace Plugin {
|
|||||||
for (const hook of await state().then((x) => x.hooks)) {
|
for (const hook of await state().then((x) => x.hooks)) {
|
||||||
const fn = hook[name]
|
const fn = hook[name]
|
||||||
if (!fn) continue
|
if (!fn) continue
|
||||||
try {
|
|
||||||
// @ts-expect-error if you feel adventurous, please fix the typing, make sure to bump the try-counter if you
|
// @ts-expect-error if you feel adventurous, please fix the typing, make sure to bump the try-counter if you
|
||||||
// give up.
|
// give up.
|
||||||
// try-counter: 2
|
// try-counter: 2
|
||||||
await fn(input, output)
|
await fn(input, output)
|
||||||
} catch (e) {
|
|
||||||
log.error("failed to trigger hook", { name, error: e })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import { Log } from "../util/log"
|
|||||||
import { Flag } from "@/flag/flag"
|
import { Flag } from "@/flag/flag"
|
||||||
import { Session } from "../session"
|
import { Session } from "../session"
|
||||||
import { work } from "../util/queue"
|
import { work } from "../util/queue"
|
||||||
|
import { fn } from "@opencode-ai/util/fn"
|
||||||
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
|
import { iife } from "@/util/iife"
|
||||||
|
import { GlobalBus } from "@/bus/global"
|
||||||
|
|
||||||
export namespace Project {
|
export namespace Project {
|
||||||
const log = Log.create({ service: "project" })
|
const log = Log.create({ service: "project" })
|
||||||
@@ -14,10 +18,17 @@ export namespace Project {
|
|||||||
.object({
|
.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
worktree: z.string(),
|
worktree: z.string(),
|
||||||
vcsDir: z.string().optional(),
|
|
||||||
vcs: z.literal("git").optional(),
|
vcs: z.literal("git").optional(),
|
||||||
|
name: z.string().optional(),
|
||||||
|
icon: z
|
||||||
|
.object({
|
||||||
|
url: z.string().optional(),
|
||||||
|
color: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
time: z.object({
|
time: z.object({
|
||||||
created: z.number(),
|
created: z.number(),
|
||||||
|
updated: z.number(),
|
||||||
initialized: z.number().optional(),
|
initialized: z.number().optional(),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -26,25 +37,19 @@ export namespace Project {
|
|||||||
})
|
})
|
||||||
export type Info = z.infer<typeof Info>
|
export type Info = z.infer<typeof Info>
|
||||||
|
|
||||||
|
export const Event = {
|
||||||
|
Updated: BusEvent.define("project.updated", Info),
|
||||||
|
}
|
||||||
|
|
||||||
export async function fromDirectory(directory: string) {
|
export async function fromDirectory(directory: string) {
|
||||||
log.info("fromDirectory", { directory })
|
log.info("fromDirectory", { directory })
|
||||||
|
|
||||||
|
const { id, worktree, vcs } = await iife(async () => {
|
||||||
const matches = Filesystem.up({ targets: [".git"], start: directory })
|
const matches = Filesystem.up({ targets: [".git"], start: directory })
|
||||||
const git = await matches.next().then((x) => x.value)
|
const git = await matches.next().then((x) => x.value)
|
||||||
await matches.return()
|
await matches.return()
|
||||||
if (!git) {
|
if (git) {
|
||||||
const project: Info = {
|
|
||||||
id: "global",
|
|
||||||
worktree: "/",
|
|
||||||
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
|
|
||||||
time: {
|
|
||||||
created: Date.now(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
await Storage.write<Info>(["project", "global"], project)
|
|
||||||
return project
|
|
||||||
}
|
|
||||||
let worktree = path.dirname(git)
|
let worktree = path.dirname(git)
|
||||||
const timer = log.time("git.rev-parse")
|
|
||||||
let id = await Bun.file(path.join(git, "opencode"))
|
let id = await Bun.file(path.join(git, "opencode"))
|
||||||
.text()
|
.text()
|
||||||
.then((x) => x.trim())
|
.then((x) => x.trim())
|
||||||
@@ -65,35 +70,90 @@ export namespace Project {
|
|||||||
id = roots[0]
|
id = roots[0]
|
||||||
if (id) Bun.file(path.join(git, "opencode")).write(id)
|
if (id) Bun.file(path.join(git, "opencode")).write(id)
|
||||||
}
|
}
|
||||||
timer.stop()
|
if (!id)
|
||||||
|
return {
|
||||||
|
id: "global",
|
||||||
|
worktree,
|
||||||
|
vcs: "git",
|
||||||
|
}
|
||||||
worktree = await $`git rev-parse --show-toplevel`
|
worktree = await $`git rev-parse --show-toplevel`
|
||||||
.quiet()
|
.quiet()
|
||||||
.nothrow()
|
.nothrow()
|
||||||
.cwd(worktree)
|
.cwd(worktree)
|
||||||
.text()
|
.text()
|
||||||
.then((x) => path.resolve(worktree, x.trim()))
|
.then((x) => path.resolve(worktree, x.trim()))
|
||||||
const vcsDir = await $`git rev-parse --git-dir`
|
return { id, worktree, vcs: "git" }
|
||||||
.quiet()
|
|
||||||
.nothrow()
|
|
||||||
.cwd(worktree)
|
|
||||||
.text()
|
|
||||||
.then((x) => path.resolve(worktree, x.trim()))
|
|
||||||
const projectID = id || "global"
|
|
||||||
const existing = id ? await Storage.read<Info>(["project", id]).catch(() => undefined) : undefined
|
|
||||||
if (!existing && id) {
|
|
||||||
await migrateFromGlobal(projectID, worktree)
|
|
||||||
}
|
}
|
||||||
const project: Info = {
|
|
||||||
id: projectID,
|
return {
|
||||||
|
id: "global",
|
||||||
|
worktree: "/",
|
||||||
|
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let existing = await Storage.read<Info>(["project", id]).catch(() => undefined)
|
||||||
|
if (!existing) {
|
||||||
|
existing = {
|
||||||
|
id,
|
||||||
worktree,
|
worktree,
|
||||||
vcsDir,
|
vcs: vcs as Info["vcs"],
|
||||||
vcs: "git",
|
|
||||||
time: {
|
time: {
|
||||||
created: Date.now(),
|
created: Date.now(),
|
||||||
|
updated: Date.now(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
await Storage.write<Info>(["project", projectID], project)
|
if (id !== "global") {
|
||||||
return project
|
await migrateFromGlobal(id, worktree)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Flag.OPENCODE_EXPERIMENTAL) discover(existing)
|
||||||
|
const result: Info = {
|
||||||
|
...existing,
|
||||||
|
worktree,
|
||||||
|
vcs: vcs as Info["vcs"],
|
||||||
|
time: {
|
||||||
|
...existing.time,
|
||||||
|
updated: Date.now(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await Storage.write<Info>(["project", id], result)
|
||||||
|
GlobalBus.emit("event", {
|
||||||
|
payload: {
|
||||||
|
type: Event.Updated.type,
|
||||||
|
properties: result,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function discover(input: Info) {
|
||||||
|
if (input.vcs !== "git") return
|
||||||
|
if (input.icon?.url) return
|
||||||
|
const glob = new Bun.Glob("**/{favicon}.{ico,png,svg,jpg,jpeg,webp}")
|
||||||
|
const matches = await Array.fromAsync(
|
||||||
|
glob.scan({
|
||||||
|
cwd: input.worktree,
|
||||||
|
absolute: true,
|
||||||
|
onlyFiles: true,
|
||||||
|
followSymlinks: false,
|
||||||
|
dot: false,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const shortest = matches.sort((a, b) => a.length - b.length)[0]
|
||||||
|
if (!shortest) return
|
||||||
|
const file = Bun.file(shortest)
|
||||||
|
const buffer = await file.arrayBuffer()
|
||||||
|
const base64 = Buffer.from(buffer).toString("base64")
|
||||||
|
const mime = file.type || "image/png"
|
||||||
|
const url = `data:${mime};base64,${base64}`
|
||||||
|
await update({
|
||||||
|
projectID: input.id,
|
||||||
|
icon: {
|
||||||
|
url,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
async function migrateFromGlobal(newProjectID: string, worktree: string) {
|
async function migrateFromGlobal(newProjectID: string, worktree: string) {
|
||||||
@@ -130,4 +190,32 @@ export namespace Project {
|
|||||||
const keys = await Storage.list(["project"])
|
const keys = await Storage.list(["project"])
|
||||||
return await Promise.all(keys.map((x) => Storage.read<Info>(x)))
|
return await Promise.all(keys.map((x) => Storage.read<Info>(x)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const update = fn(
|
||||||
|
z.object({
|
||||||
|
projectID: z.string(),
|
||||||
|
name: z.string().optional(),
|
||||||
|
icon: Info.shape.icon.optional(),
|
||||||
|
}),
|
||||||
|
async (input) => {
|
||||||
|
const result = await Storage.update<Info>(["project", input.projectID], (draft) => {
|
||||||
|
if (input.name !== undefined) draft.name = input.name
|
||||||
|
if (input.icon !== undefined) {
|
||||||
|
draft.icon = {
|
||||||
|
...draft.icon,
|
||||||
|
}
|
||||||
|
if (input.icon.url !== undefined) draft.icon.url = input.icon.url
|
||||||
|
if (input.icon.color !== undefined) draft.icon.color = input.icon.color
|
||||||
|
}
|
||||||
|
draft.time.updated = Date.now()
|
||||||
|
})
|
||||||
|
GlobalBus.emit("event", {
|
||||||
|
payload: {
|
||||||
|
type: Event.Updated.type,
|
||||||
|
properties: result,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
|
import { Bus } from "@/bus"
|
||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Log } from "@/util/log"
|
import { Log } from "@/util/log"
|
||||||
import { Bus } from "@/bus"
|
|
||||||
import { Instance } from "./instance"
|
import { Instance } from "./instance"
|
||||||
import { FileWatcher } from "@/file/watcher"
|
import { FileWatcher } from "@/file/watcher"
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ const log = Log.create({ service: "vcs" })
|
|||||||
|
|
||||||
export namespace Vcs {
|
export namespace Vcs {
|
||||||
export const Event = {
|
export const Event = {
|
||||||
BranchUpdated: Bus.event(
|
BranchUpdated: BusEvent.define(
|
||||||
"vcs.branch.updated",
|
"vcs.branch.updated",
|
||||||
z.object({
|
z.object({
|
||||||
branch: z.string().optional(),
|
branch: z.string().optional(),
|
||||||
@@ -39,16 +40,14 @@ export namespace Vcs {
|
|||||||
|
|
||||||
const state = Instance.state(
|
const state = Instance.state(
|
||||||
async () => {
|
async () => {
|
||||||
const vcsDir = Instance.project.vcsDir
|
if (Instance.project.vcs !== "git") {
|
||||||
if (Instance.project.vcs !== "git" || !vcsDir) {
|
|
||||||
return { branch: async () => undefined, unsubscribe: undefined }
|
return { branch: async () => undefined, unsubscribe: undefined }
|
||||||
}
|
}
|
||||||
let current = await currentBranch()
|
let current = await currentBranch()
|
||||||
log.info("initialized", { branch: current })
|
log.info("initialized", { branch: current })
|
||||||
|
|
||||||
const head = path.join(vcsDir, "HEAD")
|
|
||||||
const unsubscribe = Bus.subscribe(FileWatcher.Event.Updated, async (evt) => {
|
const unsubscribe = Bus.subscribe(FileWatcher.Event.Updated, async (evt) => {
|
||||||
if (evt.properties.file !== head) return
|
if (evt.properties.file.endsWith("HEAD")) return
|
||||||
const next = await currentBranch()
|
const next = await currentBranch()
|
||||||
if (next !== current) {
|
if (next !== current) {
|
||||||
log.info("branch changed", { from: current, to: next })
|
log.info("branch changed", { from: current, to: next })
|
||||||
|
|||||||
@@ -12,11 +12,22 @@ export namespace ModelsDev {
|
|||||||
export const Model = z.object({
|
export const Model = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
family: z.string().optional(),
|
||||||
release_date: z.string(),
|
release_date: z.string(),
|
||||||
attachment: z.boolean(),
|
attachment: z.boolean(),
|
||||||
reasoning: z.boolean(),
|
reasoning: z.boolean(),
|
||||||
temperature: z.boolean(),
|
temperature: z.boolean(),
|
||||||
tool_call: z.boolean(),
|
tool_call: z.boolean(),
|
||||||
|
interleaved: z
|
||||||
|
.union([
|
||||||
|
z.literal(true),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
field: z.enum(["reasoning_content", "reasoning_details"]),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
])
|
||||||
|
.optional(),
|
||||||
cost: z
|
cost: z
|
||||||
.object({
|
.object({
|
||||||
input: z.number(),
|
input: z.number(),
|
||||||
|
|||||||
@@ -330,6 +330,7 @@ export namespace Provider {
|
|||||||
npm: z.string(),
|
npm: z.string(),
|
||||||
}),
|
}),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
family: z.string().optional(),
|
||||||
capabilities: z.object({
|
capabilities: z.object({
|
||||||
temperature: z.boolean(),
|
temperature: z.boolean(),
|
||||||
reasoning: z.boolean(),
|
reasoning: z.boolean(),
|
||||||
@@ -349,6 +350,12 @@ export namespace Provider {
|
|||||||
video: z.boolean(),
|
video: z.boolean(),
|
||||||
pdf: z.boolean(),
|
pdf: z.boolean(),
|
||||||
}),
|
}),
|
||||||
|
interleaved: z.union([
|
||||||
|
z.boolean(),
|
||||||
|
z.object({
|
||||||
|
field: z.enum(["reasoning_content", "reasoning_details"]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
}),
|
}),
|
||||||
cost: z.object({
|
cost: z.object({
|
||||||
input: z.number(),
|
input: z.number(),
|
||||||
@@ -401,6 +408,7 @@ export namespace Provider {
|
|||||||
id: model.id,
|
id: model.id,
|
||||||
providerID: provider.id,
|
providerID: provider.id,
|
||||||
name: model.name,
|
name: model.name,
|
||||||
|
family: model.family,
|
||||||
api: {
|
api: {
|
||||||
id: model.id,
|
id: model.id,
|
||||||
url: provider.api!,
|
url: provider.api!,
|
||||||
@@ -450,6 +458,7 @@ export namespace Provider {
|
|||||||
video: model.modalities?.output?.includes("video") ?? false,
|
video: model.modalities?.output?.includes("video") ?? false,
|
||||||
pdf: model.modalities?.output?.includes("pdf") ?? false,
|
pdf: model.modalities?.output?.includes("pdf") ?? false,
|
||||||
},
|
},
|
||||||
|
interleaved: model.interleaved ?? false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -567,6 +576,7 @@ export namespace Provider {
|
|||||||
video: model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false,
|
video: model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false,
|
||||||
pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false,
|
pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false,
|
||||||
},
|
},
|
||||||
|
interleaved: model.interleaved ?? false,
|
||||||
},
|
},
|
||||||
cost: {
|
cost: {
|
||||||
input: model?.cost?.input ?? existingModel?.cost?.input ?? 0,
|
input: model?.cost?.input ?? existingModel?.cost?.input ?? 0,
|
||||||
|
|||||||
@@ -74,23 +74,23 @@ export namespace ProviderTransform {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeepSeek: Handle reasoning_content for tool call continuations
|
if (
|
||||||
// - With tool calls: Include reasoning_content in providerOptions so model can continue reasoning
|
model.providerID === "deepseek" ||
|
||||||
// - Without tool calls: Strip reasoning (new turn doesn't need previous reasoning)
|
model.api.id.toLowerCase().includes("deepseek") ||
|
||||||
// See: https://api-docs.deepseek.com/guides/thinking_mode
|
(model.capabilities.interleaved &&
|
||||||
if (model.providerID === "deepseek" || model.api.id.toLowerCase().includes("deepseek")) {
|
typeof model.capabilities.interleaved === "object" &&
|
||||||
|
model.capabilities.interleaved.field === "reasoning_content")
|
||||||
|
) {
|
||||||
return msgs.map((msg) => {
|
return msgs.map((msg) => {
|
||||||
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
||||||
const reasoningParts = msg.content.filter((part: any) => part.type === "reasoning")
|
const reasoningParts = msg.content.filter((part: any) => part.type === "reasoning")
|
||||||
const hasToolCalls = msg.content.some((part: any) => part.type === "tool-call")
|
|
||||||
const reasoningText = reasoningParts.map((part: any) => part.text).join("")
|
const reasoningText = reasoningParts.map((part: any) => part.text).join("")
|
||||||
|
|
||||||
// Filter out reasoning parts from content
|
// Filter out reasoning parts from content
|
||||||
const filteredContent = msg.content.filter((part: any) => part.type !== "reasoning")
|
const filteredContent = msg.content.filter((part: any) => part.type !== "reasoning")
|
||||||
|
|
||||||
// If this message has tool calls and reasoning, include reasoning_content
|
// Include reasoning_content directly on the message for all assistant messages
|
||||||
// so DeepSeek can continue reasoning after tool execution
|
if (reasoningText) {
|
||||||
if (hasToolCalls && reasoningText) {
|
|
||||||
return {
|
return {
|
||||||
...msg,
|
...msg,
|
||||||
content: filteredContent,
|
content: filteredContent,
|
||||||
@@ -104,12 +104,12 @@ export namespace ProviderTransform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For final answers (no tool calls), just strip reasoning
|
|
||||||
return {
|
return {
|
||||||
...msg,
|
...msg,
|
||||||
content: filteredContent,
|
content: filteredContent,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return msg
|
return msg
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -212,22 +212,26 @@ export namespace ProviderTransform {
|
|||||||
): Record<string, any> {
|
): Record<string, any> {
|
||||||
const result: Record<string, any> = {}
|
const result: Record<string, any> = {}
|
||||||
|
|
||||||
// switch to providerID later, for now use this
|
|
||||||
if (model.api.npm === "@openrouter/ai-sdk-provider") {
|
if (model.api.npm === "@openrouter/ai-sdk-provider") {
|
||||||
result["usage"] = {
|
result["usage"] = {
|
||||||
include: true,
|
include: true,
|
||||||
}
|
}
|
||||||
|
if (model.api.id.includes("gemini-3")) {
|
||||||
|
result["reasoning"] = { effort: "high" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.providerID === "baseten") {
|
||||||
|
result["chat_template_args"] = { enable_thinking: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.providerID === "openai" || providerOptions?.setCacheKey) {
|
if (model.providerID === "openai" || providerOptions?.setCacheKey) {
|
||||||
result["promptCacheKey"] = sessionID
|
result["promptCacheKey"] = sessionID
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (model.api.npm === "@ai-sdk/google" || model.api.npm === "@ai-sdk/google-vertex") {
|
||||||
model.providerID === "google" ||
|
|
||||||
(model.providerID.startsWith("opencode") && model.api.id.includes("gemini-3"))
|
|
||||||
) {
|
|
||||||
result["thinkingConfig"] = {
|
result["thinkingConfig"] = {
|
||||||
|
thinkingLevel: "high",
|
||||||
includeThoughts: true,
|
includeThoughts: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -273,8 +277,8 @@ export namespace ProviderTransform {
|
|||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
export function providerOptions(npm: string | undefined, providerID: string, options: { [x: string]: any }) {
|
export function providerOptions(model: Provider.Model, options: { [x: string]: any }) {
|
||||||
switch (npm) {
|
switch (model.api.npm) {
|
||||||
case "@ai-sdk/openai":
|
case "@ai-sdk/openai":
|
||||||
case "@ai-sdk/azure":
|
case "@ai-sdk/azure":
|
||||||
return {
|
return {
|
||||||
@@ -302,7 +306,7 @@ export namespace ProviderTransform {
|
|||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
[providerID]: options,
|
[model.providerID]: options,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
|
import { Bus } from "@/bus"
|
||||||
import { type IPty } from "bun-pty"
|
import { type IPty } from "bun-pty"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Identifier } from "../id/id"
|
import { Identifier } from "../id/id"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { Bus } from "../bus"
|
|
||||||
import type { WSContext } from "hono/ws"
|
import type { WSContext } from "hono/ws"
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
import { shell } from "@opencode-ai/util/shell"
|
import { shell } from "@opencode-ai/util/shell"
|
||||||
@@ -73,10 +74,10 @@ export namespace Pty {
|
|||||||
export type UpdateInput = z.infer<typeof UpdateInput>
|
export type UpdateInput = z.infer<typeof UpdateInput>
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Created: Bus.event("pty.created", z.object({ info: Info })),
|
Created: BusEvent.define("pty.created", z.object({ info: Info })),
|
||||||
Updated: Bus.event("pty.updated", z.object({ info: Info })),
|
Updated: BusEvent.define("pty.updated", z.object({ info: Info })),
|
||||||
Exited: Bus.event("pty.exited", z.object({ id: Identifier.schema("pty"), exitCode: z.number() })),
|
Exited: BusEvent.define("pty.exited", z.object({ id: Identifier.schema("pty"), exitCode: z.number() })),
|
||||||
Deleted: Bus.event("pty.deleted", z.object({ id: Identifier.schema("pty") })),
|
Deleted: BusEvent.define("pty.deleted", z.object({ id: Identifier.schema("pty") })),
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ActiveSession {
|
interface ActiveSession {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
import { describeRoute } from "hono-openapi"
|
import { describeRoute, validator } from "hono-openapi"
|
||||||
import { resolver } from "hono-openapi"
|
import { resolver } from "hono-openapi"
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
import { Project } from "../project/project"
|
import { Project } from "../project/project"
|
||||||
|
import z from "zod"
|
||||||
|
import { errors } from "./error"
|
||||||
|
|
||||||
export const ProjectRoute = new Hono()
|
export const ProjectRoute = new Hono()
|
||||||
.get(
|
.get(
|
||||||
@@ -48,3 +50,30 @@ export const ProjectRoute = new Hono()
|
|||||||
return c.json(Instance.project)
|
return c.json(Instance.project)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.patch(
|
||||||
|
"/:projectID",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Update project",
|
||||||
|
description: "Update project properties such as name, icon and color.",
|
||||||
|
operationId: "project.update",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Updated project information",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(Project.Info),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...errors(400, 404),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
validator("param", z.object({ projectID: z.string() })),
|
||||||
|
validator("json", Project.update.schema.omit({ projectID: true })),
|
||||||
|
async (c) => {
|
||||||
|
const projectID = c.req.valid("param").projectID
|
||||||
|
const body = c.req.valid("json")
|
||||||
|
const project = await Project.update({ ...body, projectID })
|
||||||
|
return c.json(project)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
|
import { Bus } from "@/bus"
|
||||||
|
import { GlobalBus } from "@/bus/global"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { Bus } from "../bus"
|
|
||||||
import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
|
import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
|
||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
import { cors } from "hono/cors"
|
import { cors } from "hono/cors"
|
||||||
@@ -8,7 +10,7 @@ import { proxy } from "hono/proxy"
|
|||||||
import { Session } from "../session"
|
import { Session } from "../session"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Provider } from "../provider/provider"
|
import { Provider } from "../provider/provider"
|
||||||
import { mapValues, pipe } from "remeda"
|
import { mapValues } from "remeda"
|
||||||
import { NamedError } from "@opencode-ai/util/error"
|
import { NamedError } from "@opencode-ai/util/error"
|
||||||
import { ModelsDev } from "../provider/models"
|
import { ModelsDev } from "../provider/models"
|
||||||
import { Ripgrep } from "../file/ripgrep"
|
import { Ripgrep } from "../file/ripgrep"
|
||||||
@@ -41,7 +43,6 @@ import type { ContentfulStatusCode } from "hono/utils/http-status"
|
|||||||
import { TuiEvent } from "@/cli/cmd/tui/event"
|
import { TuiEvent } from "@/cli/cmd/tui/event"
|
||||||
import { Snapshot } from "@/snapshot"
|
import { Snapshot } from "@/snapshot"
|
||||||
import { SessionSummary } from "@/session/summary"
|
import { SessionSummary } from "@/session/summary"
|
||||||
import { GlobalBus } from "@/bus/global"
|
|
||||||
import { SessionStatus } from "@/session/status"
|
import { SessionStatus } from "@/session/status"
|
||||||
import { upgradeWebSocket, websocket } from "hono/bun"
|
import { upgradeWebSocket, websocket } from "hono/bun"
|
||||||
import { errors } from "./error"
|
import { errors } from "./error"
|
||||||
@@ -54,7 +55,7 @@ export namespace Server {
|
|||||||
const log = Log.create({ service: "server" })
|
const log = Log.create({ service: "server" })
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Connected: Bus.event("server.connected", z.object({})),
|
Connected: BusEvent.define("server.connected", z.object({})),
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = new Hono()
|
const app = new Hono()
|
||||||
@@ -109,7 +110,7 @@ export namespace Server {
|
|||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
directory: z.string(),
|
directory: z.string(),
|
||||||
payload: Bus.payloads(),
|
payload: BusEvent.payloads(),
|
||||||
})
|
})
|
||||||
.meta({
|
.meta({
|
||||||
ref: "GlobalEvent",
|
ref: "GlobalEvent",
|
||||||
@@ -1459,12 +1460,15 @@ export namespace Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const providers = mapValues(filteredProviders, (x) => Provider.fromModelsDevProvider(x))
|
const connected = await Provider.list()
|
||||||
const connected = await Provider.list().then((x) => Object.keys(x))
|
const providers = Object.assign(
|
||||||
|
mapValues(filteredProviders, (x) => Provider.fromModelsDevProvider(x)),
|
||||||
|
connected,
|
||||||
|
)
|
||||||
return c.json({
|
return c.json({
|
||||||
all: Object.values(providers),
|
all: Object.values(providers),
|
||||||
default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
|
default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
|
||||||
connected,
|
connected: Object.keys(connected),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -2393,7 +2397,7 @@ export namespace Server {
|
|||||||
description: "Event stream",
|
description: "Event stream",
|
||||||
content: {
|
content: {
|
||||||
"text/event-stream": {
|
"text/event-stream": {
|
||||||
schema: resolver(Bus.payloads()),
|
schema: resolver(BusEvent.payloads()),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
|
import { Bus } from "@/bus"
|
||||||
import { wrapLanguageModel, type ModelMessage } from "ai"
|
import { wrapLanguageModel, type ModelMessage } from "ai"
|
||||||
import { Session } from "."
|
import { Session } from "."
|
||||||
import { Identifier } from "../id/id"
|
import { Identifier } from "../id/id"
|
||||||
@@ -5,7 +7,6 @@ import { Instance } from "../project/instance"
|
|||||||
import { Provider } from "../provider/provider"
|
import { Provider } from "../provider/provider"
|
||||||
import { MessageV2 } from "./message-v2"
|
import { MessageV2 } from "./message-v2"
|
||||||
import { SystemPrompt } from "./system"
|
import { SystemPrompt } from "./system"
|
||||||
import { Bus } from "../bus"
|
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { SessionPrompt } from "./prompt"
|
import { SessionPrompt } from "./prompt"
|
||||||
import { Flag } from "../flag/flag"
|
import { Flag } from "../flag/flag"
|
||||||
@@ -21,7 +22,7 @@ export namespace SessionCompaction {
|
|||||||
const log = Log.create({ service: "session.compaction" })
|
const log = Log.create({ service: "session.compaction" })
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Compacted: Bus.event(
|
Compacted: BusEvent.define(
|
||||||
"session.compacted",
|
"session.compacted",
|
||||||
z.object({
|
z.object({
|
||||||
sessionID: z.string(),
|
sessionID: z.string(),
|
||||||
@@ -140,8 +141,7 @@ export namespace SessionCompaction {
|
|||||||
// set to 0, we handle loop
|
// set to 0, we handle loop
|
||||||
maxRetries: 0,
|
maxRetries: 0,
|
||||||
providerOptions: ProviderTransform.providerOptions(
|
providerOptions: ProviderTransform.providerOptions(
|
||||||
model.api.npm,
|
model,
|
||||||
model.providerID,
|
|
||||||
pipe({}, mergeDeep(ProviderTransform.options(model, input.sessionID)), mergeDeep(model.options)),
|
pipe({}, mergeDeep(ProviderTransform.options(model, input.sessionID)), mergeDeep(model.options)),
|
||||||
),
|
),
|
||||||
headers: model.headers,
|
headers: model.headers,
|
||||||
@@ -193,7 +193,13 @@ export namespace SessionCompaction {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry },
|
experimental_telemetry: {
|
||||||
|
isEnabled: cfg.experimental?.openTelemetry,
|
||||||
|
metadata: {
|
||||||
|
userId: cfg.username ?? "unknown",
|
||||||
|
sessionId: input.sessionID,
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if (result === "continue" && input.auto) {
|
if (result === "continue" && input.auto) {
|
||||||
const continueMsg = await Session.updateMessage({
|
const continueMsg = await Session.updateMessage({
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
|
import { Bus } from "@/bus"
|
||||||
import { Decimal } from "decimal.js"
|
import { Decimal } from "decimal.js"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { type LanguageModelUsage, type ProviderMetadata } from "ai"
|
import { type LanguageModelUsage, type ProviderMetadata } from "ai"
|
||||||
import { Bus } from "../bus"
|
|
||||||
import { Config } from "../config/config"
|
import { Config } from "../config/config"
|
||||||
import { Flag } from "../flag/flag"
|
import { Flag } from "../flag/flag"
|
||||||
import { Identifier } from "../id/id"
|
import { Identifier } from "../id/id"
|
||||||
@@ -85,32 +86,32 @@ export namespace Session {
|
|||||||
export type ShareInfo = z.output<typeof ShareInfo>
|
export type ShareInfo = z.output<typeof ShareInfo>
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Created: Bus.event(
|
Created: BusEvent.define(
|
||||||
"session.created",
|
"session.created",
|
||||||
z.object({
|
z.object({
|
||||||
info: Info,
|
info: Info,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
Updated: Bus.event(
|
Updated: BusEvent.define(
|
||||||
"session.updated",
|
"session.updated",
|
||||||
z.object({
|
z.object({
|
||||||
info: Info,
|
info: Info,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
Deleted: Bus.event(
|
Deleted: BusEvent.define(
|
||||||
"session.deleted",
|
"session.deleted",
|
||||||
z.object({
|
z.object({
|
||||||
info: Info,
|
info: Info,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
Diff: Bus.event(
|
Diff: BusEvent.define(
|
||||||
"session.diff",
|
"session.diff",
|
||||||
z.object({
|
z.object({
|
||||||
sessionID: z.string(),
|
sessionID: z.string(),
|
||||||
diff: Snapshot.FileDiff.array(),
|
diff: Snapshot.FileDiff.array(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
Error: Bus.event(
|
Error: BusEvent.define(
|
||||||
"session.error",
|
"session.error",
|
||||||
z.object({
|
z.object({
|
||||||
sessionID: z.string().optional(),
|
sessionID: z.string().optional(),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
|
import { Bus } from "@/bus"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Bus } from "../bus"
|
|
||||||
import { NamedError } from "@opencode-ai/util/error"
|
import { NamedError } from "@opencode-ai/util/error"
|
||||||
import { Message } from "./message"
|
import { Message } from "./message"
|
||||||
import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai"
|
import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai"
|
||||||
@@ -375,27 +376,27 @@ export namespace MessageV2 {
|
|||||||
export type Info = z.infer<typeof Info>
|
export type Info = z.infer<typeof Info>
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Updated: Bus.event(
|
Updated: BusEvent.define(
|
||||||
"message.updated",
|
"message.updated",
|
||||||
z.object({
|
z.object({
|
||||||
info: Info,
|
info: Info,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
Removed: Bus.event(
|
Removed: BusEvent.define(
|
||||||
"message.removed",
|
"message.removed",
|
||||||
z.object({
|
z.object({
|
||||||
sessionID: z.string(),
|
sessionID: z.string(),
|
||||||
messageID: z.string(),
|
messageID: z.string(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
PartUpdated: Bus.event(
|
PartUpdated: BusEvent.define(
|
||||||
"message.part.updated",
|
"message.part.updated",
|
||||||
z.object({
|
z.object({
|
||||||
part: Part,
|
part: Part,
|
||||||
delta: z.string().optional(),
|
delta: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
PartRemoved: Bus.event(
|
PartRemoved: BusEvent.define(
|
||||||
"message.part.removed",
|
"message.part.removed",
|
||||||
z.object({
|
z.object({
|
||||||
sessionID: z.string(),
|
sessionID: z.string(),
|
||||||
|
|||||||
@@ -515,6 +515,37 @@ export namespace SessionPrompt {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const messages = [
|
||||||
|
...system.map(
|
||||||
|
(x): ModelMessage => ({
|
||||||
|
role: "system",
|
||||||
|
content: x,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
...MessageV2.toModelMessage(
|
||||||
|
msgs.filter((m) => {
|
||||||
|
if (m.info.role !== "assistant" || m.info.error === undefined) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
MessageV2.AbortedError.isInstance(m.info.error) &&
|
||||||
|
m.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
...(isLastStep
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
role: "assistant" as const,
|
||||||
|
content: MAX_STEPS,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
]
|
||||||
const result = await processor.process({
|
const result = await processor.process({
|
||||||
onError(error) {
|
onError(error) {
|
||||||
log.error("stream error", {
|
log.error("stream error", {
|
||||||
@@ -562,42 +593,12 @@ export namespace SessionPrompt {
|
|||||||
OUTPUT_TOKEN_MAX,
|
OUTPUT_TOKEN_MAX,
|
||||||
),
|
),
|
||||||
abortSignal: abort,
|
abortSignal: abort,
|
||||||
providerOptions: ProviderTransform.providerOptions(model.api.npm, model.providerID, params.options),
|
providerOptions: ProviderTransform.providerOptions(model, params.options),
|
||||||
stopWhen: stepCountIs(1),
|
stopWhen: stepCountIs(1),
|
||||||
temperature: params.temperature,
|
temperature: params.temperature,
|
||||||
topP: params.topP,
|
topP: params.topP,
|
||||||
toolChoice: isLastStep ? "none" : undefined,
|
toolChoice: isLastStep ? "none" : undefined,
|
||||||
messages: [
|
messages,
|
||||||
...system.map(
|
|
||||||
(x): ModelMessage => ({
|
|
||||||
role: "system",
|
|
||||||
content: x,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
...MessageV2.toModelMessage(
|
|
||||||
msgs.filter((m) => {
|
|
||||||
if (m.info.role !== "assistant" || m.info.error === undefined) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
MessageV2.AbortedError.isInstance(m.info.error) &&
|
|
||||||
m.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning")
|
|
||||||
) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
...(isLastStep
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
role: "assistant" as const,
|
|
||||||
content: MAX_STEPS,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
],
|
|
||||||
tools: model.capabilities.toolcall === false ? undefined : tools,
|
tools: model.capabilities.toolcall === false ? undefined : tools,
|
||||||
model: wrapLanguageModel({
|
model: wrapLanguageModel({
|
||||||
model: language,
|
model: language,
|
||||||
@@ -628,7 +629,13 @@ export namespace SessionPrompt {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry },
|
experimental_telemetry: {
|
||||||
|
isEnabled: cfg.experimental?.openTelemetry,
|
||||||
|
metadata: {
|
||||||
|
userId: cfg.username ?? "unknown",
|
||||||
|
sessionId: sessionID,
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if (result === "stop") break
|
if (result === "stop") break
|
||||||
continue
|
continue
|
||||||
@@ -1224,8 +1231,8 @@ export namespace SessionPrompt {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
await Session.updatePart(part)
|
await Session.updatePart(part)
|
||||||
const shell = process.env["SHELL"] ?? "bash"
|
const shell = process.env["SHELL"] ?? (process.platform === "win32" ? process.env["COMSPEC"] || "cmd.exe" : "bash")
|
||||||
const shellName = path.basename(shell)
|
const shellName = path.basename(shell).toLowerCase()
|
||||||
|
|
||||||
const invocations: Record<string, { args: string[] }> = {
|
const invocations: Record<string, { args: string[] }> = {
|
||||||
nu: {
|
nu: {
|
||||||
@@ -1255,6 +1262,14 @@ export namespace SessionPrompt {
|
|||||||
`,
|
`,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// Windows cmd.exe
|
||||||
|
"cmd.exe": {
|
||||||
|
args: ["/c", input.command],
|
||||||
|
},
|
||||||
|
// Windows PowerShell
|
||||||
|
"powershell.exe": {
|
||||||
|
args: ["-NoProfile", "-Command", input.command],
|
||||||
|
},
|
||||||
// Fallback: any shell that doesn't match those above
|
// Fallback: any shell that doesn't match those above
|
||||||
"": {
|
"": {
|
||||||
args: ["-c", "-l", `${input.command}`],
|
args: ["-c", "-l", `${input.command}`],
|
||||||
@@ -1266,7 +1281,7 @@ export namespace SessionPrompt {
|
|||||||
|
|
||||||
const proc = spawn(shell, args, {
|
const proc = spawn(shell, args, {
|
||||||
cwd: Instance.directory,
|
cwd: Instance.directory,
|
||||||
detached: true,
|
detached: process.platform !== "win32",
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
@@ -1458,7 +1473,7 @@ export namespace SessionPrompt {
|
|||||||
await generateText({
|
await generateText({
|
||||||
// use higher # for reasoning models since reasoning tokens eat up a lot of the budget
|
// use higher # for reasoning models since reasoning tokens eat up a lot of the budget
|
||||||
maxOutputTokens: small.capabilities.reasoning ? 3000 : 20,
|
maxOutputTokens: small.capabilities.reasoning ? 3000 : 20,
|
||||||
providerOptions: ProviderTransform.providerOptions(small.api.npm, small.providerID, options),
|
providerOptions: ProviderTransform.providerOptions(small, options),
|
||||||
messages: [
|
messages: [
|
||||||
...SystemPrompt.title(small.providerID).map(
|
...SystemPrompt.title(small.providerID).map(
|
||||||
(x): ModelMessage => ({
|
(x): ModelMessage => ({
|
||||||
@@ -1491,7 +1506,13 @@ export namespace SessionPrompt {
|
|||||||
],
|
],
|
||||||
headers: small.headers,
|
headers: small.headers,
|
||||||
model: language,
|
model: language,
|
||||||
experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry },
|
experimental_telemetry: {
|
||||||
|
isEnabled: cfg.experimental?.openTelemetry,
|
||||||
|
metadata: {
|
||||||
|
userId: cfg.username ?? "unknown",
|
||||||
|
sessionId: input.session.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (result.text)
|
if (result.text)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
import { Bus } from "@/bus"
|
import { Bus } from "@/bus"
|
||||||
import { Instance } from "@/project/instance"
|
import { Instance } from "@/project/instance"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
@@ -24,7 +25,7 @@ export namespace SessionStatus {
|
|||||||
export type Info = z.infer<typeof Info>
|
export type Info = z.infer<typeof Info>
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Status: Bus.event(
|
Status: BusEvent.define(
|
||||||
"session.status",
|
"session.status",
|
||||||
z.object({
|
z.object({
|
||||||
sessionID: z.string(),
|
sessionID: z.string(),
|
||||||
@@ -32,7 +33,7 @@ export namespace SessionStatus {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
// deprecated
|
// deprecated
|
||||||
Idle: Bus.event(
|
Idle: BusEvent.define(
|
||||||
"session.idle",
|
"session.idle",
|
||||||
z.object({
|
z.object({
|
||||||
sessionID: z.string(),
|
sessionID: z.string(),
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export namespace SessionSummary {
|
|||||||
if (textPart && !userMsg.summary?.title) {
|
if (textPart && !userMsg.summary?.title) {
|
||||||
const result = await generateText({
|
const result = await generateText({
|
||||||
maxOutputTokens: small.capabilities.reasoning ? 1500 : 20,
|
maxOutputTokens: small.capabilities.reasoning ? 1500 : 20,
|
||||||
providerOptions: ProviderTransform.providerOptions(small.api.npm, small.providerID, options),
|
providerOptions: ProviderTransform.providerOptions(small, options),
|
||||||
messages: [
|
messages: [
|
||||||
...SystemPrompt.title(small.providerID).map(
|
...SystemPrompt.title(small.providerID).map(
|
||||||
(x): ModelMessage => ({
|
(x): ModelMessage => ({
|
||||||
@@ -111,7 +111,13 @@ export namespace SessionSummary {
|
|||||||
],
|
],
|
||||||
headers: small.headers,
|
headers: small.headers,
|
||||||
model: language,
|
model: language,
|
||||||
experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry },
|
experimental_telemetry: {
|
||||||
|
isEnabled: cfg.experimental?.openTelemetry,
|
||||||
|
metadata: {
|
||||||
|
userId: cfg.username ?? "unknown",
|
||||||
|
sessionId: assistantMsg.sessionID,
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
log.info("title", { title: result.text })
|
log.info("title", { title: result.text })
|
||||||
userMsg.summary.title = result.text
|
userMsg.summary.title = result.text
|
||||||
@@ -138,7 +144,7 @@ export namespace SessionSummary {
|
|||||||
const result = await generateText({
|
const result = await generateText({
|
||||||
model: language,
|
model: language,
|
||||||
maxOutputTokens: 100,
|
maxOutputTokens: 100,
|
||||||
providerOptions: ProviderTransform.providerOptions(small.api.npm, small.providerID, options),
|
providerOptions: ProviderTransform.providerOptions(small, options),
|
||||||
messages: [
|
messages: [
|
||||||
...SystemPrompt.summarize(small.providerID).map(
|
...SystemPrompt.summarize(small.providerID).map(
|
||||||
(x): ModelMessage => ({
|
(x): ModelMessage => ({
|
||||||
@@ -153,7 +159,13 @@ export namespace SessionSummary {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
headers: small.headers,
|
headers: small.headers,
|
||||||
experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry },
|
experimental_telemetry: {
|
||||||
|
isEnabled: cfg.experimental?.openTelemetry,
|
||||||
|
metadata: {
|
||||||
|
userId: cfg.username ?? "unknown",
|
||||||
|
sessionId: assistantMsg.sessionID,
|
||||||
|
},
|
||||||
|
},
|
||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
if (result) summary = result.text
|
if (result) summary = result.text
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
|
import { Bus } from "@/bus"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Bus } from "../bus"
|
|
||||||
import { Storage } from "../storage/storage"
|
import { Storage } from "../storage/storage"
|
||||||
|
|
||||||
export namespace Todo {
|
export namespace Todo {
|
||||||
@@ -14,7 +15,7 @@ export namespace Todo {
|
|||||||
export type Info = z.infer<typeof Info>
|
export type Info = z.infer<typeof Info>
|
||||||
|
|
||||||
export const Event = {
|
export const Event = {
|
||||||
Updated: Bus.event(
|
Updated: BusEvent.define(
|
||||||
"todo.updated",
|
"todo.updated",
|
||||||
z.object({
|
z.object({
|
||||||
sessionID: z.string(),
|
sessionID: z.string(),
|
||||||
|
|||||||
@@ -16,12 +16,8 @@ import { Flag } from "@/flag/flag.ts"
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { iife } from "@/util/iife"
|
import { iife } from "@/util/iife"
|
||||||
|
|
||||||
const DEFAULT_MAX_OUTPUT_LENGTH = 30_000
|
const MAX_OUTPUT_LENGTH = Flag.OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH || 30_000
|
||||||
const MAX_OUTPUT_LENGTH = (() => {
|
const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000
|
||||||
const parsed = Number(Flag.OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH)
|
|
||||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_OUTPUT_LENGTH
|
|
||||||
})()
|
|
||||||
const DEFAULT_TIMEOUT = 2 * 60 * 1000
|
|
||||||
const SIGKILL_TIMEOUT_MS = 200
|
const SIGKILL_TIMEOUT_MS = 200
|
||||||
|
|
||||||
export const log = Log.create({ service: "bash-tool" })
|
export const log = Log.create({ service: "bash-tool" })
|
||||||
|
|||||||
@@ -18,13 +18,47 @@ Before executing the command, please follow these steps:
|
|||||||
|
|
||||||
Usage notes:
|
Usage notes:
|
||||||
- The command argument is required.
|
- The command argument is required.
|
||||||
- You can specify an optional timeout in milliseconds. If not specified, commands will timeout after 120000ms (2 minutes). Use the `timeout` parameter to control execution time.
|
- You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes).
|
||||||
- The `workdir` parameter specifies the working directory for the command. Defaults to the current working directory. Prefer setting `workdir` over using `cd` in your commands.
|
If not specified, commands will timeout after 120000ms (2 minutes).
|
||||||
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
|
- It is very helpful if you write a clear, concise description of what this command
|
||||||
- If the output exceeds 30000 characters, output will be truncated before being returned to you.
|
does in 5-10 words.
|
||||||
- VERY IMPORTANT: You MUST avoid using search commands like `find` and `grep`. Instead use Grep, Glob, or Task to search. You MUST avoid read tools like `cat`, `head`, `tail`, and `ls`, and use Read and List to read files.
|
- If the output exceeds 30000 characters, output will be truncated before being
|
||||||
- If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` (or /usr/bin/rg) first, which all opencode users have pre-installed.
|
returned to you.
|
||||||
- When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).
|
- You can use the `run_in_background` parameter to run the command in the background,
|
||||||
|
which allows you to continue working while the command runs. You can monitor the output
|
||||||
|
using the Bash tool as it becomes available. You do not need to use '&' at the end of
|
||||||
|
the command when using this parameter.
|
||||||
|
|
||||||
|
- Avoid using Bash with the `find`, `grep`, `cat`, `head`, `tail`, `sed`, `awk`, or
|
||||||
|
`echo` commands, unless explicitly instructed or when these commands are truly necessary
|
||||||
|
for the task. Instead, always prefer using the dedicated tools for these commands:
|
||||||
|
- File search: Use Glob (NOT find or ls)
|
||||||
|
- Content search: Use Grep (NOT grep or rg)
|
||||||
|
- Read files: Use Read (NOT cat/head/tail)
|
||||||
|
- Edit files: Use Edit (NOT sed/awk)
|
||||||
|
- Write files: Use Write (NOT echo >/cat <<EOF)
|
||||||
|
- Communication: Output text directly (NOT echo/printf)
|
||||||
|
- When issuing multiple commands:
|
||||||
|
- If the commands are independent and can run in parallel, make multiple Bash tool
|
||||||
|
calls in a single message. For example, if you need to run "git status" and "git diff",
|
||||||
|
send a single message with two Bash tool calls in parallel.
|
||||||
|
- If the commands depend on each other and must run sequentially, use a single Bash
|
||||||
|
call with '&&' to chain them together (e.g., `git add . && git commit -m "message" &&
|
||||||
|
git push`). For instance, if one operation must complete before another starts (like
|
||||||
|
mkdir before cp, Write before Bash for git operations, or git add before git commit),
|
||||||
|
run these operations sequentially instead.
|
||||||
|
- Use ';' only when you need to run commands sequentially but don't care if earlier
|
||||||
|
commands fail
|
||||||
|
- DO NOT use newlines to separate commands (newlines are ok in quoted strings)
|
||||||
|
- Try to maintain your current working directory throughout the session by using
|
||||||
|
absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly
|
||||||
|
requests it.
|
||||||
|
<good-example>
|
||||||
|
pytest /foo/bar/tests
|
||||||
|
</good-example>
|
||||||
|
<bad-example>
|
||||||
|
cd /foo/bar && pytest tests
|
||||||
|
</bad-example>
|
||||||
|
|
||||||
# Working Directory
|
# Working Directory
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
- Searches file contents using regular expressions
|
- Searches file contents using regular expressions
|
||||||
- Supports full regex syntax (eg. "log.*Error", "function\s+\w+", etc.)
|
- Supports full regex syntax (eg. "log.*Error", "function\s+\w+", etc.)
|
||||||
- Filter files by pattern with the include parameter (eg. "*.js", "*.{ts,tsx}")
|
- Filter files by pattern with the include parameter (eg. "*.js", "*.{ts,tsx}")
|
||||||
- Returns file paths with at least one match sorted by modification time
|
- Returns file paths and line numbers with at least one match sorted by modification time
|
||||||
- Use this tool when you need to find files containing specific patterns
|
- Use this tool when you need to find files containing specific patterns
|
||||||
- If you need to identify/count the number of matches within files, use the Bash tool with `rg` (ripgrep) directly. Do NOT use `grep`.
|
- If you need to identify/count the number of matches within files, use the Bash tool with `rg` (ripgrep) directly. Do NOT use `grep`.
|
||||||
- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Task tool instead
|
- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Task tool instead
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export const ReadTool = Tool.define("read", {
|
|||||||
throw new Error(`File not found: ${filepath}`)
|
throw new Error(`File not found: ${filepath}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isImage = file.type.startsWith("image/")
|
const isImage = file.type.startsWith("image/") && file.type !== "image/svg+xml"
|
||||||
const isPdf = file.type === "application/pdf"
|
const isPdf = file.type === "application/pdf"
|
||||||
if (isImage || isPdf) {
|
if (isImage || isPdf) {
|
||||||
const mime = file.type
|
const mime = file.type
|
||||||
|
|||||||
@@ -1,27 +1,33 @@
|
|||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
import { realpathSync } from "fs"
|
import * as fs from "fs/promises"
|
||||||
import os from "os"
|
import os from "os"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
|
// Strip null bytes from paths (defensive fix for CI environment issues)
|
||||||
|
function sanitizePath(p: string): string {
|
||||||
|
return p.replace(/\0/g, "")
|
||||||
|
}
|
||||||
|
|
||||||
type TmpDirOptions<T> = {
|
type TmpDirOptions<T> = {
|
||||||
git?: boolean
|
git?: boolean
|
||||||
init?: (dir: string) => Promise<T>
|
init?: (dir: string) => Promise<T>
|
||||||
dispose?: (dir: string) => Promise<T>
|
dispose?: (dir: string) => Promise<T>
|
||||||
}
|
}
|
||||||
export async function tmpdir<T>(options?: TmpDirOptions<T>) {
|
export async function tmpdir<T>(options?: TmpDirOptions<T>) {
|
||||||
const dirpath = path.join(os.tmpdir(), "opencode-test-" + Math.random().toString(36).slice(2))
|
const dirpath = sanitizePath(path.join(os.tmpdir(), "opencode-test-" + Math.random().toString(36).slice(2)))
|
||||||
await $`mkdir -p ${dirpath}`.quiet()
|
await fs.mkdir(dirpath, { recursive: true })
|
||||||
if (options?.git) {
|
if (options?.git) {
|
||||||
await $`git init`.cwd(dirpath).quiet()
|
await $`git init`.cwd(dirpath).quiet()
|
||||||
await $`git commit --allow-empty -m "root commit ${dirpath}"`.cwd(dirpath).quiet()
|
await $`git commit --allow-empty -m "root commit ${dirpath}"`.cwd(dirpath).quiet()
|
||||||
}
|
}
|
||||||
const extra = await options?.init?.(dirpath)
|
const extra = await options?.init?.(dirpath)
|
||||||
|
const realpath = sanitizePath(await fs.realpath(dirpath))
|
||||||
const result = {
|
const result = {
|
||||||
[Symbol.asyncDispose]: async () => {
|
[Symbol.asyncDispose]: async () => {
|
||||||
await options?.dispose?.(dirpath)
|
await options?.dispose?.(dirpath)
|
||||||
await $`rm -rf ${dirpath}`.quiet()
|
// await fs.rm(dirpath, { recursive: true, force: true })
|
||||||
},
|
},
|
||||||
path: realpathSync(dirpath),
|
path: realpath,
|
||||||
extra: extra as T,
|
extra: extra as T,
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import { test, expect, mock, beforeEach } from "bun:test"
|
||||||
|
|
||||||
|
// Track what options were passed to each transport constructor
|
||||||
|
const transportCalls: Array<{
|
||||||
|
type: "streamable" | "sse"
|
||||||
|
url: string
|
||||||
|
options: { authProvider?: unknown; requestInit?: RequestInit }
|
||||||
|
}> = []
|
||||||
|
|
||||||
|
// Mock the transport constructors to capture their arguments
|
||||||
|
mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
|
||||||
|
StreamableHTTPClientTransport: class MockStreamableHTTP {
|
||||||
|
constructor(url: URL, options?: { authProvider?: unknown; requestInit?: RequestInit }) {
|
||||||
|
transportCalls.push({
|
||||||
|
type: "streamable",
|
||||||
|
url: url.toString(),
|
||||||
|
options: options ?? {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async start() {
|
||||||
|
throw new Error("Mock transport cannot connect")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({
|
||||||
|
SSEClientTransport: class MockSSE {
|
||||||
|
constructor(url: URL, options?: { authProvider?: unknown; requestInit?: RequestInit }) {
|
||||||
|
transportCalls.push({
|
||||||
|
type: "sse",
|
||||||
|
url: url.toString(),
|
||||||
|
options: options ?? {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async start() {
|
||||||
|
throw new Error("Mock transport cannot connect")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
transportCalls.length = 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// Import MCP after mocking
|
||||||
|
const { MCP } = await import("../../src/mcp/index")
|
||||||
|
const { Instance } = await import("../../src/project/instance")
|
||||||
|
const { tmpdir } = await import("../fixture/fixture")
|
||||||
|
|
||||||
|
test("headers are passed to transports when oauth is enabled (default)", async () => {
|
||||||
|
await using tmp = await tmpdir({
|
||||||
|
init: async (dir) => {
|
||||||
|
await Bun.write(
|
||||||
|
`${dir}/opencode.json`,
|
||||||
|
JSON.stringify({
|
||||||
|
$schema: "https://opencode.ai/config.json",
|
||||||
|
mcp: {
|
||||||
|
"test-server": {
|
||||||
|
type: "remote",
|
||||||
|
url: "https://example.com/mcp",
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test-token",
|
||||||
|
"X-Custom-Header": "custom-value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
fn: async () => {
|
||||||
|
// Trigger MCP initialization - it will fail to connect but we can check the transport options
|
||||||
|
await MCP.add("test-server", {
|
||||||
|
type: "remote",
|
||||||
|
url: "https://example.com/mcp",
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test-token",
|
||||||
|
"X-Custom-Header": "custom-value",
|
||||||
|
},
|
||||||
|
}).catch(() => {})
|
||||||
|
|
||||||
|
// Both transports should have been created with headers
|
||||||
|
expect(transportCalls.length).toBeGreaterThanOrEqual(1)
|
||||||
|
|
||||||
|
for (const call of transportCalls) {
|
||||||
|
expect(call.options.requestInit).toBeDefined()
|
||||||
|
expect(call.options.requestInit?.headers).toEqual({
|
||||||
|
Authorization: "Bearer test-token",
|
||||||
|
"X-Custom-Header": "custom-value",
|
||||||
|
})
|
||||||
|
// OAuth should be enabled by default, so authProvider should exist
|
||||||
|
expect(call.options.authProvider).toBeDefined()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("headers are passed to transports when oauth is explicitly disabled", async () => {
|
||||||
|
await using tmp = await tmpdir()
|
||||||
|
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
fn: async () => {
|
||||||
|
transportCalls.length = 0
|
||||||
|
|
||||||
|
await MCP.add("test-server-no-oauth", {
|
||||||
|
type: "remote",
|
||||||
|
url: "https://example.com/mcp",
|
||||||
|
oauth: false,
|
||||||
|
headers: {
|
||||||
|
Authorization: "Bearer test-token",
|
||||||
|
},
|
||||||
|
}).catch(() => {})
|
||||||
|
|
||||||
|
expect(transportCalls.length).toBeGreaterThanOrEqual(1)
|
||||||
|
|
||||||
|
for (const call of transportCalls) {
|
||||||
|
expect(call.options.requestInit).toBeDefined()
|
||||||
|
expect(call.options.requestInit?.headers).toEqual({
|
||||||
|
Authorization: "Bearer test-token",
|
||||||
|
})
|
||||||
|
// OAuth is disabled, so no authProvider
|
||||||
|
expect(call.options.authProvider).toBeUndefined()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("no requestInit when headers are not provided", async () => {
|
||||||
|
await using tmp = await tmpdir()
|
||||||
|
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
fn: async () => {
|
||||||
|
transportCalls.length = 0
|
||||||
|
|
||||||
|
await MCP.add("test-server-no-headers", {
|
||||||
|
type: "remote",
|
||||||
|
url: "https://example.com/mcp",
|
||||||
|
}).catch(() => {})
|
||||||
|
|
||||||
|
expect(transportCalls.length).toBeGreaterThanOrEqual(1)
|
||||||
|
|
||||||
|
for (const call of transportCalls) {
|
||||||
|
// No headers means requestInit should be undefined
|
||||||
|
expect(call.options.requestInit).toBeUndefined()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -2,12 +2,14 @@
|
|||||||
// xdg-basedir reads env vars at import time, so we must set these first
|
// xdg-basedir reads env vars at import time, so we must set these first
|
||||||
import os from "os"
|
import os from "os"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
import fs from "fs/promises"
|
||||||
|
|
||||||
const testDataDir = path.join(os.tmpdir(), "opencode-test-data-" + process.pid)
|
const dir = path.join(os.tmpdir(), "opencode-test-data-" + process.pid)
|
||||||
process.env["XDG_DATA_HOME"] = testDataDir
|
await fs.mkdir(dir, { recursive: true })
|
||||||
process.env["XDG_CACHE_HOME"] = path.join(testDataDir, "cache")
|
process.env["XDG_DATA_HOME"] = path.join(dir, "share")
|
||||||
process.env["XDG_CONFIG_HOME"] = path.join(testDataDir, "config")
|
process.env["XDG_CACHE_HOME"] = path.join(dir, "cache")
|
||||||
process.env["XDG_STATE_HOME"] = path.join(testDataDir, "state")
|
process.env["XDG_CONFIG_HOME"] = path.join(dir, "config")
|
||||||
|
process.env["XDG_STATE_HOME"] = path.join(dir, "state")
|
||||||
|
|
||||||
// Clear provider env vars to ensure clean test state
|
// Clear provider env vars to ensure clean test state
|
||||||
delete process.env["ANTHROPIC_API_KEY"]
|
delete process.env["ANTHROPIC_API_KEY"]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { Project } from "../../src/project/project"
|
import { Project } from "../../src/project/project"
|
||||||
import { Log } from "../../src/util/log"
|
import { Log } from "../../src/util/log"
|
||||||
|
import { Storage } from "../../src/storage/storage"
|
||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { tmpdir } from "../fixture/fixture"
|
import { tmpdir } from "../fixture/fixture"
|
||||||
@@ -18,7 +19,6 @@ describe("Project.fromDirectory", () => {
|
|||||||
expect(project.id).toBe("global")
|
expect(project.id).toBe("global")
|
||||||
expect(project.vcs).toBe("git")
|
expect(project.vcs).toBe("git")
|
||||||
expect(project.worktree).toBe(tmp.path)
|
expect(project.worktree).toBe(tmp.path)
|
||||||
expect(project.vcsDir).toBe(path.join(tmp.path, ".git"))
|
|
||||||
|
|
||||||
const opencodeFile = path.join(tmp.path, ".git", "opencode")
|
const opencodeFile = path.join(tmp.path, ".git", "opencode")
|
||||||
const fileExists = await Bun.file(opencodeFile).exists()
|
const fileExists = await Bun.file(opencodeFile).exists()
|
||||||
@@ -40,3 +40,33 @@ describe("Project.fromDirectory", () => {
|
|||||||
expect(fileExists).toBe(true)
|
expect(fileExists).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("Project.discover", () => {
|
||||||
|
test("should discover favicon.png in root", async () => {
|
||||||
|
await using tmp = await tmpdir({ git: true })
|
||||||
|
const project = await Project.fromDirectory(tmp.path)
|
||||||
|
|
||||||
|
const pngData = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
|
||||||
|
await Bun.write(path.join(tmp.path, "favicon.png"), pngData)
|
||||||
|
|
||||||
|
await Project.discover(project)
|
||||||
|
|
||||||
|
const updated = await Storage.read<Project.Info>(["project", project.id])
|
||||||
|
expect(updated.icon).toBeDefined()
|
||||||
|
expect(updated.icon?.url).toStartWith("data:")
|
||||||
|
expect(updated.icon?.url).toContain("base64")
|
||||||
|
expect(updated.icon?.color).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should not discover non-image files", async () => {
|
||||||
|
await using tmp = await tmpdir({ git: true })
|
||||||
|
const project = await Project.fromDirectory(tmp.path)
|
||||||
|
|
||||||
|
await Bun.write(path.join(tmp.path, "favicon.txt"), "not an image")
|
||||||
|
|
||||||
|
await Project.discover(project)
|
||||||
|
|
||||||
|
const updated = await Storage.read<Project.Info>(["project", project.id])
|
||||||
|
expect(updated.icon).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => {
|
|||||||
toolcall: true,
|
toolcall: true,
|
||||||
input: { text: true, audio: false, image: false, video: false, pdf: false },
|
input: { text: true, audio: false, image: false, video: false, pdf: false },
|
||||||
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
||||||
|
interleaved: false,
|
||||||
},
|
},
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.001,
|
input: 0.001,
|
||||||
@@ -157,53 +158,6 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => {
|
|||||||
expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBe("Let me think about this...")
|
expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBe("Let me think about this...")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("DeepSeek without tool calls strips reasoning from content", () => {
|
|
||||||
const msgs = [
|
|
||||||
{
|
|
||||||
role: "assistant",
|
|
||||||
content: [
|
|
||||||
{ type: "reasoning", text: "Let me think about this..." },
|
|
||||||
{ type: "text", text: "Final answer" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
] as any[]
|
|
||||||
|
|
||||||
const result = ProviderTransform.message(msgs, {
|
|
||||||
id: "deepseek/deepseek-chat",
|
|
||||||
providerID: "deepseek",
|
|
||||||
api: {
|
|
||||||
id: "deepseek-chat",
|
|
||||||
url: "https://api.deepseek.com",
|
|
||||||
npm: "@ai-sdk/openai-compatible",
|
|
||||||
},
|
|
||||||
name: "DeepSeek Chat",
|
|
||||||
capabilities: {
|
|
||||||
temperature: true,
|
|
||||||
reasoning: true,
|
|
||||||
attachment: false,
|
|
||||||
toolcall: true,
|
|
||||||
input: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
||||||
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
||||||
},
|
|
||||||
cost: {
|
|
||||||
input: 0.001,
|
|
||||||
output: 0.002,
|
|
||||||
cache: { read: 0.0001, write: 0.0002 },
|
|
||||||
},
|
|
||||||
limit: {
|
|
||||||
context: 128000,
|
|
||||||
output: 8192,
|
|
||||||
},
|
|
||||||
status: "active",
|
|
||||||
options: {},
|
|
||||||
headers: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result).toHaveLength(1)
|
|
||||||
expect(result[0].content).toEqual([{ type: "text", text: "Final answer" }])
|
|
||||||
expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBeUndefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
test("DeepSeek model ID containing 'deepseek' matches (case insensitive)", () => {
|
test("DeepSeek model ID containing 'deepseek' matches (case insensitive)", () => {
|
||||||
const msgs = [
|
const msgs = [
|
||||||
{
|
{
|
||||||
@@ -236,6 +190,7 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => {
|
|||||||
toolcall: true,
|
toolcall: true,
|
||||||
input: { text: true, audio: false, image: false, video: false, pdf: false },
|
input: { text: true, audio: false, image: false, video: false, pdf: false },
|
||||||
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
||||||
|
interleaved: false,
|
||||||
},
|
},
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.001,
|
input: 0.001,
|
||||||
@@ -281,6 +236,7 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => {
|
|||||||
toolcall: true,
|
toolcall: true,
|
||||||
input: { text: true, audio: false, image: true, video: false, pdf: false },
|
input: { text: true, audio: false, image: true, video: false, pdf: false },
|
||||||
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
||||||
|
interleaved: false,
|
||||||
},
|
},
|
||||||
cost: {
|
cost: {
|
||||||
input: 0.03,
|
input: 0.03,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "@opencode-ai/plugin",
|
"name": "@opencode-ai/plugin",
|
||||||
"version": "1.0.137",
|
"version": "1.0.143",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsgo --noEmit",
|
"typecheck": "tsgo --noEmit",
|
||||||
|
|||||||
@@ -17,5 +17,5 @@ for (const [key, value] of Object.entries(pkg.exports)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await Bun.write("package.json", JSON.stringify(pkg, null, 2))
|
await Bun.write("package.json", JSON.stringify(pkg, null, 2))
|
||||||
await $`bun publish --tag ${Script.channel} --access public`
|
await $`bun pm pack && npm publish *.tgz --tag ${Script.channel} --access public`
|
||||||
await Bun.write("package.json", JSON.stringify(original, null, 2))
|
await Bun.write("package.json", JSON.stringify(original, null, 2))
|
||||||
|
|||||||
@@ -13,10 +13,21 @@ if (process.versions.bun !== expectedBunVersion) {
|
|||||||
throw new Error(`This script requires bun@${expectedBunVersion}, but you are using bun@${process.versions.bun}`)
|
throw new Error(`This script requires bun@${expectedBunVersion}, but you are using bun@${process.versions.bun}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CHANNEL = process.env["OPENCODE_CHANNEL"] ?? (await $`git branch --show-current`.text().then((x) => x.trim()))
|
const env = {
|
||||||
|
OPENCODE_CHANNEL: process.env["OPENCODE_CHANNEL"],
|
||||||
|
OPENCODE_BUMP: process.env["OPENCODE_BUMP"],
|
||||||
|
OPENCODE_VERSION: process.env["OPENCODE_VERSION"],
|
||||||
|
}
|
||||||
|
const CHANNEL = await (async () => {
|
||||||
|
if (env.OPENCODE_CHANNEL) return env.OPENCODE_CHANNEL
|
||||||
|
if (env.OPENCODE_BUMP) return "latest"
|
||||||
|
if (env.OPENCODE_VERSION && !env.OPENCODE_VERSION.startsWith("0.0.0-")) return "latest"
|
||||||
|
return await $`git branch --show-current`.text().then((x) => x.trim())
|
||||||
|
})()
|
||||||
const IS_PREVIEW = CHANNEL !== "latest"
|
const IS_PREVIEW = CHANNEL !== "latest"
|
||||||
|
|
||||||
const VERSION = await (async () => {
|
const VERSION = await (async () => {
|
||||||
if (process.env["OPENCODE_VERSION"]) return process.env["OPENCODE_VERSION"]
|
if (env.OPENCODE_VERSION) return env.OPENCODE_VERSION
|
||||||
if (IS_PREVIEW) return `0.0.0-${CHANNEL}-${new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "")}`
|
if (IS_PREVIEW) return `0.0.0-${CHANNEL}-${new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "")}`
|
||||||
const version = await fetch("https://registry.npmjs.org/opencode-ai/latest")
|
const version = await fetch("https://registry.npmjs.org/opencode-ai/latest")
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
@@ -25,7 +36,7 @@ const VERSION = await (async () => {
|
|||||||
})
|
})
|
||||||
.then((data: any) => data.version)
|
.then((data: any) => data.version)
|
||||||
const [major, minor, patch] = version.split(".").map((x: string) => Number(x) || 0)
|
const [major, minor, patch] = version.split(".").map((x: string) => Number(x) || 0)
|
||||||
const t = process.env["OPENCODE_BUMP"]?.toLowerCase()
|
const t = env.OPENCODE_BUMP?.toLowerCase()
|
||||||
if (t === "major") return `${major + 1}.0.0`
|
if (t === "major") return `${major + 1}.0.0`
|
||||||
if (t === "minor") return `${major}.${minor + 1}.0`
|
if (t === "minor") return `${major}.${minor + 1}.0`
|
||||||
return `${major}.${minor}.${patch + 1}`
|
return `${major}.${minor}.${patch + 1}`
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user