Compare commits

..

119 Commits

Author SHA1 Message Date
Aiden Cline
090f638318 ignore 2025-12-09 16:29:55 -06:00
Aiden Cline
8f3f87943a fmt 2025-12-09 16:29:33 -06:00
Aiden Cline
7ac3f9182b fixes 2025-12-09 16:23:18 -06:00
Aiden Cline
cda2138f24 fixes 2025-12-09 16:22:36 -06:00
Aiden Cline
c4f8c6c005 fix 2025-12-09 15:59:03 -06:00
Aiden Cline
8304bd418f Merge branch 'dev' into interleaved-thinking 2025-12-09 15:54:28 -06:00
Aiden Cline
8a9c7a4ef3 add OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT 2025-12-09 15:44:56 -06:00
Adam
2dad56c9a2 wip(desktop): progress 2025-12-09 15:39:44 -06:00
Aiden Cline
366b6e4fbb wip 2025-12-09 15:36:53 -06:00
Adam
41d78c1ecc wip(desktop): progress 2025-12-09 15:32:36 -06:00
ry2009
16c4b02b69 feat: add Biome LSP support (#5290) 2025-12-09 15:31:13 -06:00
Adam
35c04d9283 wip(desktop): progress 2025-12-09 15:24:11 -06:00
Adam
1fbd7a7f9a wip(desktop): progress 2025-12-09 15:21:47 -06:00
Adam
d7563d1694 wip(desktop): progress 2025-12-09 15:21:47 -06:00
Adam
b9fa7d9163 wip(desktop): progress 2025-12-09 15:21:47 -06:00
Adam
f736751ab2 wip(desktop): progress 2025-12-09 15:21:45 -06:00
Aiden Cline
dbcc779f0b ci: fix sdk workflow 2025-12-09 15:08:30 -06:00
Nick
c33a90320c fix: resolve 'latest' to actual version when caching plugins (#5292) 2025-12-09 15:07:59 -06:00
Dax Raad
802b862aae format on write 2025-12-09 16:06:57 -05:00
GitHub Action
b0cd171c1b chore: format code 2025-12-09 21:05:09 +00:00
GitHub Action
13755f4680 chore: regen sdk 2025-12-09 21:04:37 +00:00
Dax Raad
b242659cc3 fix types 2025-12-09 16:03:58 -05:00
Dax Raad
5f6b2fdc6f fix tests 2025-12-09 16:03:33 -05:00
GitHub Action
e34f18991e chore: format code 2025-12-09 20:53:06 +00:00
GitHub Action
209b0a06f7 chore: regen sdk 2025-12-09 20:52:31 +00:00
Dax Raad
a2e460bc4b discover logic 2025-12-09 15:51:55 -05:00
GitHub Action
fc9081afe4 chore: format code 2025-12-09 20:49:25 +00:00
GitHub Action
1a3f7c3d84 chore: regen sdk 2025-12-09 20:48:49 +00:00
Dax Raad
06aa1f49b8 sync 2025-12-09 15:48:22 -05:00
GitHub Action
dfd67cd922 chore: format code 2025-12-09 20:44:09 +00:00
GitHub Action
70f7287ca1 chore: regen sdk 2025-12-09 20:43:35 +00:00
Dax Raad
f1955b4d05 core: fix project event emission to include updated worktree data 2025-12-09 15:41:38 -05:00
Dax Raad
c5e5627cbd test fixes 2025-12-09 15:41:38 -05:00
GitHub Action
93378526b9 chore: format code 2025-12-09 20:26:02 +00:00
GitHub Action
abf176a335 chore: regen sdk 2025-12-09 20:25:21 +00:00
Aiden Cline
84a0868e66 fix: read when file is svg 2025-12-09 14:24:30 -06:00
GitHub Action
75a9c42789 chore: format code 2025-12-09 20:20:03 +00:00
GitHub Action
204fa54625 chore: regen sdk 2025-12-09 20:19:27 +00:00
Dax Raad
365584048f core: fix project creation to include updated timestamp 2025-12-09 15:18:55 -05:00
Dax Raad
edffcc32cf core: make project updated timestamp optional to support legacy project data 2025-12-09 15:18:55 -05:00
GitHub Action
238f441bcb chore: format code 2025-12-09 20:17:37 +00:00
GitHub Action
0571a8302c chore: regen sdk 2025-12-09 20:16:59 +00:00
Aiden Cline
8c07382382 ci: fix sdk gen 2025-12-09 14:16:22 -06:00
GitHub Action
fa32fbd187 chore: format code 2025-12-09 20:12:07 +00:00
GitHub Action
0fd2ecd0ba chore: regen sdk 2025-12-09 20:11:32 +00:00
Dax Raad
7439a40b00 core: fix project icon update to preserve existing icon properties 2025-12-09 15:11:00 -05:00
GitHub Action
2ad99713f3 chore: format code 2025-12-09 20:07:32 +00:00
GitHub Action
19ec970701 chore: regen sdk 2025-12-09 20:06:57 +00:00
Dax Raad
b48caec218 core: add automatic project icon discovery from favicon/logo files 2025-12-09 15:06:24 -05:00
GitHub Action
380c34af53 chore: format code 2025-12-09 19:54:54 +00:00
GitHub Action
553d9013eb chore: regen sdk 2025-12-09 19:54:19 +00:00
Dax Raad
8bff3cdae8 fix ci 2025-12-09 14:53:47 -05:00
Dax Raad
0b40c3d37d rework project loading 2025-12-09 14:41:14 -05:00
Dax Raad
1e3bdcc71c rename bus 2025-12-09 14:32:09 -05:00
GitHub Action
de577e17da chore: format code 2025-12-09 19:31:07 +00:00
GitHub Action
8a9e258ad7 chore: regen sdk 2025-12-09 19:30:32 +00:00
Adam
9a34965432 feat: add color to project 2025-12-09 13:29:59 -06:00
Adam
c944d19c3b wip(desktop): progress 2025-12-09 13:24:37 -06:00
Dax Raad
fb1b6c5e6b add project.name/icon 2025-12-09 13:57:18 -05:00
GitHub Action
ad0c4c5d89 chore: format code 2025-12-09 18:37:14 +00:00
GitHub Action
a54b663a39 chore: regen sdk 2025-12-09 18:36:38 +00:00
Adam
ae4993f39a wip(desktop): progress 2025-12-09 12:36:06 -06:00
GitHub Action
aa638cec48 chore: format code 2025-12-09 18:10:31 +00:00
GitHub Action
4db4a90559 chore: regen sdk 2025-12-09 18:07:43 +00:00
Aiden Cline
e23a81097c core: add test to prevent MCP headers regression when OAuth is enabled
Custom headers configured for remote MCP servers were being silently
dropped when OAuth was enabled (the default). This test ensures headers
are always sent to MCP servers regardless of OAuth configuration.
2025-12-09 12:06:40 -06:00
opencode
76f4803d8d release: v1.0.138 2025-12-09 18:05:21 +00:00
GitHub Action
22e4649318 chore: format code 2025-12-09 17:53:53 +00:00
GitHub Action
0ac70ff261 chore: regen sdk 2025-12-09 17:53:14 +00:00
Adam
1bc1e56da3 wip(desktop): progress 2025-12-09 11:52:43 -06:00
GitHub Action
0d0c20e673 chore: format code 2025-12-09 17:45:35 +00:00
Jay
a964824b22 docs: Modify documentation for SDK ecosystem references
Updated link text to refer to community-built projects.
2025-12-09 12:44:33 -05:00
GitHub Action
2cf0d578fe chore: regen sdk 2025-12-09 17:43:58 +00:00
Jay
13e8fb382f docs: Update community plugins reference in documentation 2025-12-09 12:43:29 -05:00
GitHub Action
4090bc9dea chore: format code 2025-12-09 17:39:25 +00:00
Aiden Cline
cec1caf99e ci: sdk stuff 2025-12-09 11:38:22 -06:00
GitHub Action
c74da97d52 chore: regen sdk 2025-12-09 17:38:01 +00:00
Jay
1f2497ce69 docs: Add submission note for OpenCode projects
Added a note about submitting projects to the list.
2025-12-09 12:37:34 -05:00
GitHub Action
986f14cb15 chore: format code 2025-12-09 17:37:32 +00:00
GitHub Action
34f639d510 chore: regen sdk 2025-12-09 17:34:45 +00:00
Aiden Cline
defe51c825 docs: fix name 2025-12-09 11:34:15 -06:00
GitHub Action
5a16acef8c chore: format code 2025-12-09 17:26:28 +00:00
Aiden Cline
2ce249dbc0 docs: OpenCode ecosystem (#5287)
Co-authored-by: GitHub Action <action@github.com>
2025-12-09 11:25:28 -06:00
GitHub Action
7ba6b18945 chore: format code 2025-12-09 17:10:13 +00:00
GitHub Action
b8c0b393bf chore: regen sdk 2025-12-09 17:09:35 +00:00
Adam
5442adb517 wip(desktop): progress 2025-12-09 11:09:00 -06:00
Adam
6b2ac20abc wip(desktop): progress 2025-12-09 11:09:00 -06:00
GitHub Action
3efc95b157 chore: format code 2025-12-09 16:53:58 +00:00
GitHub Action
cd9db8a81d chore: regen sdk 2025-12-09 16:53:24 +00:00
Dax Raad
036f5d4eef core: add project update timestamps to track when projects were last modified
Projects now track when they were last updated, making it easier for users
to see recent activity and identify stale projects in their workspace.
2025-12-09 11:52:53 -05:00
GitHub Action
c4401290db chore: format code 2025-12-09 15:53:48 +00:00
GitHub Action
4a6deb6420 chore: regen sdk 2025-12-09 15:53:12 +00:00
André Cruz
87a03e1e30 fix(mcp): send custom headers regardless of OAuth settings (#5273)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-09 09:52:39 -06:00
GitHub Action
01dc9d7ec6 chore: format code 2025-12-09 15:47:54 +00:00
GitHub Action
e78e0f9841 chore: regen sdk 2025-12-09 15:47:19 +00:00
Ravi Kumar
8326640670 feat(telemetry): Add userId and sessionId metadata to experimental_telemetry (#5279) 2025-12-09 09:46:48 -06:00
GitHub Action
d079af4be2 chore: format code 2025-12-09 12:16:05 +00:00
GitHub Action
82c9584382 chore: regen sdk 2025-12-09 12:15:30 +00:00
Adam
d3b6de855b chore: cleanup 2025-12-09 06:14:58 -06:00
Adam
5ad000fd99 chore: cleanup 2025-12-09 06:14:22 -06:00
Adam
fe196da430 fix(tui): order 2025-12-09 06:13:36 -06:00
Adam
20662e2101 wip(desktop): progress 2025-12-09 06:12:09 -06:00
Adam
0a357be160 wip(desktop): progress 2025-12-09 06:12:09 -06:00
Adam
d29205e677 fix: diff scroll gutter 2025-12-09 06:12:09 -06:00
GitHub Action
9d0630f094 ignore: update download stats 2025-12-09 2025-12-09 12:04:56 +00:00
Github Action
b6844565e8 Update Nix flake.lock and hashes 2025-12-09 11:15:34 +00:00
GitHub Action
17d1b24def chore: format code 2025-12-09 11:15:02 +00:00
GitHub Action
3d279edf44 chore: regen sdk 2025-12-09 11:14:23 +00:00
Brendan Allan
0a47a3cea0 fix: use ts project references for desktop and tauri 2025-12-09 19:13:51 +08:00
Github Action
306d57fcde Update Nix flake.lock and hashes 2025-12-09 09:17:58 +00:00
GitHub Action
ff6f1abf61 chore: format code 2025-12-09 09:17:36 +00:00
GitHub Action
331278a5be chore: regen sdk 2025-12-09 09:17:00 +00:00
Brendan Allan
78547f3c59 desktop: move updater logic to js 2025-12-09 17:16:24 +08:00
GitHub Action
d32671224f chore: format code 2025-12-09 07:26:55 +00:00
GitHub Action
9ade416ad4 chore: regen sdk 2025-12-09 07:26:16 +00:00
Aiden Cline
f8bd4ff705 core: refactor providerOptions function to accept Provider.Model for cleaner API 2025-12-09 01:25:36 -06:00
GitHub Action
2206e10d92 chore: format code 2025-12-09 06:42:56 +00:00
Aiden Cline
e282d5dc42 ci: run format workflow after sdk workflow completion 2025-12-09 00:41:40 -06:00
GitHub Action
2b4a5aede1 chore: regen sdk 2025-12-09 06:37:35 +00:00
Aiden Cline
654a2cd6a4 core: remove unused fzf dependency to address CVE
- Eliminates fzf binary dependency that was no longer used after file search overhaul
- Removes fzf from Nix package configuration and Arch Linux PKGBUILD dependencies
2025-12-09 00:36:33 -06:00
119 changed files with 3334 additions and 3478 deletions

View File

@@ -8,9 +8,14 @@ on:
branches-ignore:
- production
workflow_dispatch:
workflow_run:
workflows: ["sdk"]
types:
- completed
jobs:
format:
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.event.workflow_run.conclusion == 'success'
permissions:
contents: write
steps:

View File

@@ -18,6 +18,8 @@ jobs:
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
ref: ${{ github.event.pull_request.head.ref || github.ref_name }}
- name: Setup Bun
uses: ./.github/actions/setup-bun
@@ -26,6 +28,7 @@ jobs:
run: |
bun ./packages/sdk/js/script/build.ts
(cd packages/opencode && bun dev generate > ../sdk/openapi.json)
bun x prettier --write packages/sdk/js/src/openapi.ts
if [ -z "$(git status --porcelain)" ]; then
echo "No changes to commit"
exit 0
@@ -34,6 +37,7 @@ jobs:
git config --local user.name "GitHub Action"
git add -A
git commit -m "chore: regen sdk"
git push --no-verify
git push origin HEAD:${PUSH_BRANCH} --no-verify
env:
CI: true
PUSH_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }}

1
.gitignore vendored
View File

@@ -9,6 +9,7 @@ node_modules
playground
tmp
dist
ts-dist
.turbo
**/.serena
.serena/

View File

@@ -1,5 +1,6 @@
---
description: git commit and push
model: opencode/glm-4.6
---
commit and push

View File

@@ -7,9 +7,7 @@
"instructions": ["STYLE_GUIDE.md"],
"provider": {
"opencode": {
"options": {
// "baseURL": "http://localhost:8080",
},
"options": {},
},
},
"mcp": {

329
STATS.md
View File

@@ -1,166 +1,167 @@
# Download Stats
| Date | GitHub Downloads | npm Downloads | Total |
| ---------- | ------------------ | ----------------- | ------------------- |
| 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-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) |
| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) |
| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) |
| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) |
| 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-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) |
| Date | GitHub Downloads | npm Downloads | Total |
| ---------- | ------------------- | ----------------- | ------------------- |
| 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-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) |
| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) |
| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) |
| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) |
| 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-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) |

View File

@@ -20,7 +20,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.0.137",
"version": "1.0.138",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -48,7 +48,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.0.137",
"version": "1.0.138",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -75,7 +75,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.0.137",
"version": "1.0.138",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -99,7 +99,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.0.137",
"version": "1.0.138",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -123,7 +123,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.0.137",
"version": "1.0.138",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -168,7 +168,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.0.137",
"version": "1.0.138",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -196,7 +196,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.0.137",
"version": "1.0.138",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
@@ -212,7 +212,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.0.137",
"version": "1.0.138",
"bin": {
"opencode": "./bin/opencode",
},
@@ -304,7 +304,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.0.137",
"version": "1.0.138",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -324,7 +324,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.0.137",
"version": "1.0.138",
"devDependencies": {
"@hey-api/openapi-ts": "0.88.1",
"@tsconfig/node22": "catalog:",
@@ -335,7 +335,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.0.137",
"version": "1.0.138",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -348,12 +348,13 @@
},
"packages/tauri": {
"name": "@opencode-ai/tauri",
"version": "1.0.137",
"version": "1.0.138",
"dependencies": {
"@opencode-ai/desktop": "workspace:*",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-process": "~2",
"@tauri-apps/plugin-shell": "~2",
"@tauri-apps/plugin-updater": "~2",
"solid-js": "catalog:",
@@ -362,13 +363,14 @@
"@actions/artifact": "4.0.0",
"@tauri-apps/cli": "^2",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
"typescript": "~5.6.2",
"vite": "catalog:",
},
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.0.137",
"version": "1.0.138",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -400,7 +402,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "1.0.137",
"version": "1.0.138",
"dependencies": {
"zod": "catalog:",
},
@@ -411,7 +413,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.0.137",
"version": "1.0.138",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -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-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-updater": ["@tauri-apps/plugin-updater@2.9.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg=="],

View File

@@ -1,3 +1,3 @@
{
"nodeModules": "sha256-IzF5XDY09Z1p/8jgYIHhE/jpKPub15KKUpV+a/aKpuc="
"nodeModules": "sha256-lM/7mkrPHz5E6YOMjWspfRhKjwav9ANrLt9kYlpPkEI="
}

View File

@@ -1,4 +1,4 @@
{ lib, stdenvNoCC, bun, fzf, ripgrep, makeBinaryWrapper }:
{ lib, stdenvNoCC, bun, ripgrep, makeBinaryWrapper }:
args:
let
scripts = args.scripts;
@@ -97,7 +97,7 @@ stdenvNoCC.mkDerivation (finalAttrs: {
makeWrapper ${bun}/bin/bun $out/bin/opencode \
--add-flags "run" \
--add-flags "$out/lib/opencode/dist/src/index.js" \
--prefix PATH : ${lib.makeBinPath [ fzf ripgrep ]} \
--prefix PATH : ${lib.makeBinPath [ ripgrep ]} \
--argv0 opencode
runHook postInstall

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.0.137",
"version": "1.0.138",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.0.137",
"version": "1.0.138",
"private": true,
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.0.137",
"version": "1.0.138",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.0.137",
"version": "1.0.138",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/desktop",
"version": "1.0.137",
"version": "1.0.138",
"description": "",
"type": "module",
"exports": {

View File

@@ -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
}

View File

@@ -2,19 +2,18 @@ import "@/index.css"
import { Router, Route, Navigate } from "@solidjs/router"
import { MetaProvider } from "@solidjs/meta"
import { Font } from "@opencode-ai/ui/font"
import { Favicon } from "@opencode-ai/ui/favicon"
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
import { DiffComponentProvider } from "@opencode-ai/ui/context/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 Home from "@/pages/home"
import DirectoryLayout from "@/pages/directory-layout"
import Session from "@/pages/session"
import { LayoutProvider } from "./context/layout"
import { GlobalSDKProvider } from "./context/global-sdk"
import { SessionProvider } from "./context/session"
import { base64Encode } from "@opencode-ai/util/encode"
import { createMemo, Show } from "solid-js"
import { Show } from "solid-js"
const host = import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "127.0.0.1"
const port = import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"
@@ -25,7 +24,7 @@ const url =
? `http://${host}:${port}`
: "/")
export function DesktopInterface() {
export function App() {
return (
<MarkedProvider>
<DiffComponentProvider component={Diff}>
@@ -35,14 +34,7 @@ export function DesktopInterface() {
<MetaProvider>
<Font />
<Router root={Layout}>
<Route
path="/"
component={() => {
const globalSync = useGlobalSync()
const slug = createMemo(() => base64Encode(globalSync.data.defaultProject!.worktree))
return <Navigate href={`${slug()}/session`} />
}}
/>
<Route path="/" component={Home} />
<Route path="/:dir" component={DirectoryLayout}>
<Route path="/" component={() => <Navigate href="session" />} />
<Route

View File

@@ -19,7 +19,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
sdk.global.event().then(async (events) => {
for await (const event of events.stream) {
// console.log("event", event)
emitter.emit(event.directory, event.payload)
emitter.emit(event.directory ?? "global", event.payload)
}
})

View File

@@ -22,7 +22,7 @@ type State = {
ready: boolean
provider: Provider[]
agent: Agent[]
project: Project
project: string
config: Config
path: Path
session: Session[]
@@ -51,7 +51,6 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
init: () => {
const [globalStore, setGlobalStore] = createStore<{
ready: boolean
defaultProject?: Project // TODO: remove this when we can select projects
projects: Project[]
children: Record<string, State>
}>({
@@ -61,11 +60,10 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
})
const children: Record<string, ReturnType<typeof createStore<State>>> = {}
function child(directory: string) {
if (!children[directory]) {
setGlobalStore("children", directory, {
project: { id: "", worktree: "", time: { created: 0, initialized: 0 } },
project: "",
config: {},
path: { state: "", config: "", worktree: "", directory: "" },
ready: false,
@@ -75,7 +73,7 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
session_status: {},
session_diff: {},
todo: {},
limit: 10,
limit: 5,
message: {},
part: {},
node: [],
@@ -89,9 +87,29 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
const sdk = useGlobalSDK()
sdk.event.listen((e) => {
const directory = e.name
const [store, setStore] = child(directory)
const event = e.details
if (directory === "global") {
switch (event.type) {
case "project.updated": {
const result = Binary.search(globalStore.projects, event.properties.id, (s) => s.id)
if (result.found) {
setGlobalStore("projects", result.index, reconcile(event.properties))
break
}
setGlobalStore(
"projects",
produce((draft) => {
draft.splice(result.index, 0, event.properties)
}),
)
break
}
}
return
}
const [store, setStore] = child(directory)
switch (event.type) {
case "session.updated": {
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
@@ -165,11 +183,11 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
sdk.client.project.list().then((x) =>
setGlobalStore(
"projects",
x.data!.filter((x) => !x.worktree.includes("opencode-test")),
x
.data!.filter((x) => !x.worktree.includes("opencode-test") && x.vcs)
.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))
return {

View File

@@ -1,18 +1,20 @@
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 { makePersisted } from "@solid-primitives/storage"
import { useGlobalSync } from "./global-sync"
import { useGlobalSDK } from "./global-sdk"
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
name: "Layout",
init: () => {
const globalSdk = useGlobalSDK()
const globalSync = useGlobalSync()
const [store, setStore] = makePersisted(
createStore({
projects: [] as { directory: string; expanded: boolean }[],
projects: [] as { worktree: string; expanded: boolean }[],
sidebar: {
opened: true,
opened: false,
width: 280,
},
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 {
projects: {
list: createMemo(() =>
globalSync.data.defaultProject
? [{ directory: globalSync.data.defaultProject!.worktree, expanded: true }, ...store.projects]
: store.projects,
),
list: createMemo(() => store.projects.flatMap(enrich)),
open(directory: string) {
if (store.projects.find((x) => x.directory === directory)) return
setStore("projects", (x) => [...x, { directory, expanded: true }])
if (store.projects.find((x) => x.worktree === directory)) return
loadProjectSessions(directory)
setStore("projects", (x) => [...x, { worktree: directory, expanded: true }])
},
close(directory: string) {
setStore("projects", (x) => x.filter((x) => x.directory !== directory))
setStore("projects", (x) => x.filter((x) => x.worktree !== directory))
},
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) {
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: {

View File

@@ -257,7 +257,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const load = async (path: string) => {
const relativePath = relative(path)
sdk.client.file.read({ path: relativePath }).then((x) => {
await sdk.client.file.read({ path: relativePath }).then((x) => {
setStore(
"node",
relativePath,
@@ -335,7 +335,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
return {
node: async (path: string) => {
if (!store.node[path]) {
if (!store.node[path] || !store.node[path].loaded) {
await init(path)
}
return store.node[path]

View File

@@ -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
},
})

View File

@@ -7,7 +7,6 @@ import { TextSelection } from "./local"
import { pipe, sumBy } from "remeda"
import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2"
import { useParams } from "@solidjs/router"
import { base64Encode } from "@opencode-ai/util/encode"
import { useSDK } from "./sdk"
export type LocalPTY = {
@@ -25,9 +24,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
const sdk = useSDK()
const params = useParams()
const sync = useSync()
const name = createMemo(
() => `${base64Encode(sync.data.project.worktree)}/session${params.id ? "/" + params.id : ""}.v2`,
)
const name = createMemo(() => `${params.dir}/session${params.id ? "/" + params.id : ""}.v3`)
const [store, setStore] = makePersisted(
createStore<{

View File

@@ -13,7 +13,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const [store, setStore] = globalSync.child(sdk.directory)
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)),
path: () => sdk.client.path.get().then((x) => setStore("path", 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() {
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: {
get(sessionID: string) {
const match = Binary.search(store.session, sessionID, (s) => s.id)

View File

@@ -1,7 +1,7 @@
// @refresh reload
import { render } from "solid-js/web"
import { DesktopInterface } from "@/DesktopInterface"
import { Platform, PlatformProvider } from "@/PlatformContext"
import { App } from "@/app"
import { Platform, PlatformProvider } from "@/context/platform"
const root = document.getElementById("root")
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(
() => (
<PlatformProvider value={platform}>
<DesktopInterface />
<App />
</PlatformProvider>
),
root!,

View File

@@ -1,2 +1,2 @@
export { PlatformProvider, type Platform } from "./PlatformContext"
export { DesktopInterface } from "./DesktopInterface"
export { PlatformProvider, type Platform } from "./context/platform"
export { App } from "./app"

View File

@@ -1,32 +1,31 @@
import { createMemo, type ParentProps } from "solid-js"
import { createMemo, Show, type ParentProps } from "solid-js"
import { useParams } from "@solidjs/router"
import { SDKProvider } from "@/context/sdk"
import { SyncProvider, useSync } from "@/context/sync"
import { LocalProvider } from "@/context/local"
import { useGlobalSync } from "@/context/global-sync"
import { base64Decode } from "@opencode-ai/util/encode"
import { DataProvider } from "@opencode-ai/ui/context"
import { iife } from "@opencode-ai/util/iife"
export default function Layout(props: ParentProps) {
const params = useParams()
const sync = useGlobalSync()
const directory = createMemo(() => {
const decoded = base64Decode(params.dir!)
return sync.data.projects.find((x) => x.worktree === decoded)?.worktree ?? "/"
return base64Decode(params.dir!)
})
return (
<SDKProvider directory={directory()}>
<SyncProvider>
{iife(() => {
const sync = useSync()
return (
<DataProvider data={sync.data} directory={directory()}>
<LocalProvider>{props.children}</LocalProvider>
</DataProvider>
)
})}
</SyncProvider>
</SDKProvider>
<Show when={params.dir} keyed>
<SDKProvider directory={directory()}>
<SyncProvider>
{iife(() => {
const sync = useSync()
return (
<DataProvider data={sync.data} directory={directory()}>
<LocalProvider>{props.children}</LocalProvider>
</DataProvider>
)
})}
</SyncProvider>
</SDKProvider>
</Show>
)
}

View File

@@ -1,21 +1,92 @@
import { useGlobalSync } from "@/context/global-sync"
import { base64Encode } from "@opencode-ai/util/encode"
import { For } from "solid-js"
import { A } from "@solidjs/router"
import { For, Match, Show, Switch } from "solid-js"
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() {
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 (
<div class="flex flex-col gap-3">
<For each={sync.data.projects}>
{(project) => (
<Button as={A} href={base64Encode(project.worktree)}>
{getFilename(project.worktree)}
</Button>
)}
</For>
<div class="mx-auto mt-55">
<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) => (
<Button
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>
)}
</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>
)
}

View File

@@ -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 { A, useNavigate, useParams } from "@solidjs/router"
import { useLayout } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync"
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
import { Mark } from "@opencode-ai/ui/logo"
import { Avatar } from "@opencode-ai/ui/avatar"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Button } from "@opencode-ai/ui/button"
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 { getFilename } from "@opencode-ai/util/path"
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) {
const navigate = useNavigate()
const [store, setStore] = createStore({
lastSession: {} as { [directory: string]: string },
activeDraggable: undefined as string | undefined,
})
const params = useParams()
const globalSync = useGlobalSync()
const layout = useLayout()
const platform = usePlatform()
const navigate = useNavigate()
const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
const sessions = createMemo(() => globalSync.child(currentDirectory())[0].session ?? [])
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) {
if (!session) return
navigate(`/${params.dir}/session/${session?.id}`)
}
const handleOpenProject = async () => {
// layout.projects.open(dir.)
function openProject(directory: string, navigate = true) {
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 (
@@ -50,61 +323,75 @@ export default function Layout(props: ParentProps) {
<Mark class="shrink-0" />
</A>
<div class="pl-4 px-6 flex items-center justify-between gap-4 w-full">
<div class="flex items-center gap-3">
<div class="flex items-center gap-2">
<Select
options={layout.projects.list().map((project) => getFilename(project.directory))}
current={getFilename(currentDirectory())}
class="text-14-regular text-text-base"
variant="ghost"
/>
<div class="text-text-weaker">/</div>
<Select
options={sessions()}
current={currentSession()}
placeholder="Select session"
label={(x) => x.title}
value={(x) => x.id}
onSelect={navigateToSession}
class="text-14-regular text-text-base max-w-md"
variant="ghost"
/>
<Show when={params.dir && layout.projects.list().length > 0}>
<div class="flex items-center gap-3">
<div class="flex items-center gap-2">
<Select
options={layout.projects.list().map((project) => project.worktree)}
current={currentDirectory()}
label={(x) => getFilename(x)}
onSelect={(x) => (x ? navigateToProject(x) : undefined)}
class="text-14-regular text-text-base"
variant="ghost"
>
{/* @ts-ignore */}
{(i) => (
<div class="flex items-center gap-2">
<Icon name="folder" size="small" />
<div class="text-text-strong">{getFilename(i)}</div>
</div>
)}
</Select>
<div class="text-text-weaker">/</div>
<Select
options={sessions()}
current={currentSession()}
placeholder="New session"
label={(x) => x.title}
value={(x) => x.id}
onSelect={navigateToSession}
class="text-14-regular text-text-base max-w-md"
variant="ghost"
/>
</div>
<Show when={currentSession()}>
<Button as={A} href={`/${params.dir}/session`} icon="plus-small">
New session
</Button>
</Show>
</div>
<Button as={A} href={`/${params.dir}/session`} icon="plus-small">
New session
</Button>
</div>
<div class="flex items-center gap-4">
<Tooltip
class="shrink-0"
value={
<div class="flex items-center gap-2">
<span>Toggle terminal</span>
<span class="text-icon-base text-12-medium">Ctrl `</span>
</div>
}
>
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
class="group-hover/terminal-toggle:hidden"
/>
<Icon
size="small"
name="layout-bottom-partial"
class="hidden group-hover/terminal-toggle:inline-block"
/>
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
class="hidden group-active/terminal-toggle:inline-block"
/>
</div>
</Button>
</Tooltip>
</div>
<div class="flex items-center gap-4">
<Tooltip
class="shrink-0"
value={
<div class="flex items-center gap-2">
<span>Toggle terminal</span>
<span class="text-icon-base text-12-medium">Ctrl `</span>
</div>
}
>
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
class="group-hover/terminal-toggle:hidden"
/>
<Icon
size="small"
name="layout-bottom-partial"
class="hidden group-hover/terminal-toggle:inline-block"
/>
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
class="hidden group-active/terminal-toggle:inline-block"
/>
</div>
</Button>
</Tooltip>
</div>
</Show>
</div>
</header>
<div class="h-[calc(100vh-3rem)] flex">
@@ -127,12 +414,12 @@ export default function Layout(props: ParentProps) {
onCollapse={layout.sidebar.close}
/>
</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()}>
<Button
variant="ghost"
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}
>
<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>
</Button>
</Tooltip>
<div class="flex flex-col justify-center items-start gap-4 self-stretch min-h-0">
<div class="hidden @[4rem]:flex size-full flex-col grow overflow-y-auto no-scrollbar">
<For each={layout.projects.list()}>
{(project) => {
const [store] = globalSync.child(project.directory)
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">
{getFilename(project.directory)}
</Collapsible.Trigger>
<IconButton as={A} href={`${slug()}/session`} icon="plus-small" size="normal" />
</Button>
<Collapsible.Content>
<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>
<DragDropProvider
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
collisionDetector={closestCenter}
>
<DragDropSensors />
<ConstrainDragXAxis />
<div class="w-full min-w-8 flex flex-col gap-2 min-h-0 overflow-y-auto no-scrollbar">
<SortableProvider ids={layout.projects.list().map((p) => p.worktree)}>
<For each={layout.projects.list()}>{(project) => <SortableProject project={project} />}</For>
</SortableProvider>
</div>
</div>
<DragOverlay>
<ProjectDragOverlay />
</DragOverlay>
</DragDropProvider>
</div>
<div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
<Tooltip placement="right" value="Open project" inactive={layout.sidebar.opened()}>
<Button
disabled
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px]"
variant="ghost"
size="large"
icon="folder-add-left"
onClick={handleOpenProject}
>
<Show when={layout.sidebar.opened()}>Open project</Show>
</Button>
</Tooltip>
<Show when={platform.openDirectoryPickerDialog}>
<Tooltip placement="right" value="Open project" inactive={layout.sidebar.opened()}>
<Button
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg"
variant="ghost"
size="large"
icon="folder-add-left"
onClick={chooseProject}
>
<Show when={layout.sidebar.opened()}>Open project</Show>
</Button>
</Tooltip>
</Show>
<Tooltip placement="right" value="Settings" inactive={layout.sidebar.opened()}>
<Button
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"
size="large"
icon="settings-gear"
@@ -268,7 +494,7 @@ export default function Layout(props: ParentProps) {
as={"a"}
href="https://opencode.ai/desktop-feedback"
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"
size="large"
icon="bubble-5"

View File

@@ -28,7 +28,7 @@ import {
import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
import type { JSX } from "solid-js"
import { useSync } from "@/context/sync"
import { useSession } from "@/context/session"
import { useSession, type LocalPTY } from "@/context/session"
import { useLayout } from "@/context/layout"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { Terminal } from "@/components/terminal"
@@ -43,6 +43,7 @@ export default function Page() {
clickTimer: undefined as number | undefined,
fileSelectOpen: false,
activeDraggable: undefined as string | undefined,
activeTerminalDraggable: undefined as string | undefined,
})
let inputRef!: HTMLDivElement
@@ -178,6 +179,49 @@ export default function Page() {
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 => {
return (
<div class="flex items-center gap-x-1.5">
@@ -220,7 +264,6 @@ export default function Page() {
onTabClose: (tab: string) => void
}): JSX.Element => {
const sortable = createSortable(props.tab)
const [file] = createResource(
() => props.tab,
async (tab) => {
@@ -230,7 +273,6 @@ export default function Page() {
return undefined
},
)
return (
// @ts-ignore
<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>
</div>
</div>
<div class="flex justify-center items-center gap-3">
<Icon name="pencil-line" size="small" />
<div class="text-12-medium text-text-weak">
Last modified&nbsp;
<span class="text-text-strong">
{DateTime.fromMillis(sync.data.project.time.created).toRelative()}
</span>
</div>
</div>
<Show when={sync.project}>
{(project) => (
<div class="flex justify-center items-center gap-3">
<Icon name="pencil-line" size="small" />
<div class="text-12-medium text-text-weak">
Last modified&nbsp;
<span class="text-text-strong">
{DateTime.fromMillis(project().time.updated ?? project().time.created).toRelative()}
</span>
</div>
</div>
)}
</Show>
</div>
</Match>
</Switch>
@@ -576,7 +622,6 @@ export default function Page() {
onOpenChange={(open) => setStore("fileSelectOpen", open)}
onSelect={(x) => {
if (x) {
local.file.open(x)
return session.layout.openTab("file://" + x)
}
return undefined
@@ -617,40 +662,54 @@ export default function Page() {
onResize={layout.terminal.resize}
onCollapse={layout.terminal.close}
/>
<Tabs variant="alt" value={session.terminal.active()} onChange={session.terminal.open}>
<Tabs.List class="h-10">
<DragDropProvider
onDragStart={handleTerminalDragStart}
onDragEnd={handleTerminalDragEnd}
onDragOver={handleTerminalDragOver}
collisionDetector={closestCenter}
>
<DragDropSensors />
<ConstrainDragYAxis />
<Tabs variant="alt" value={session.terminal.active()} onChange={session.terminal.open}>
<Tabs.List class="h-10">
<SortableProvider ids={session.terminal.all().map((t) => t.id)}>
<For each={session.terminal.all()}>{(terminal) => <SortableTerminalTab terminal={terminal} />}</For>
</SortableProvider>
<div class="h-full flex items-center justify-center">
<Tooltip value="Open file" class="flex items-center">
<IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={session.terminal.new} />
</Tooltip>
</div>
</Tabs.List>
<For each={session.terminal.all()}>
{(terminal) => (
<Tabs.Trigger
value={terminal.id}
closeButton={
session.terminal.all().length > 1 && (
<IconButton icon="close" variant="ghost" onClick={() => session.terminal.close(terminal.id)} />
)
}
>
{terminal.title}
</Tabs.Trigger>
<Tabs.Content value={terminal.id}>
<Terminal
pty={terminal}
onCleanup={session.terminal.update}
onConnectError={() => session.terminal.clone(terminal.id)}
/>
</Tabs.Content>
)}
</For>
<div class="h-full flex items-center justify-center">
<Tooltip value="Open file" class="flex items-center">
<IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={session.terminal.new} />
</Tooltip>
</div>
</Tabs.List>
<For each={session.terminal.all()}>
{(terminal) => (
<Tabs.Content value={terminal.id}>
<Terminal
pty={terminal}
onCleanup={session.terminal.update}
onConnectError={() => session.terminal.clone(terminal.id)}
/>
</Tabs.Content>
)}
</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>
</Show>
</div>

View File

@@ -1,6 +1,7 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"composite": true,
"target": "ESNext",
"module": "ESNext",
"skipLibCheck": true,
@@ -11,7 +12,9 @@
"jsxImportSource": "solid-js",
"allowJs": true,
"strict": true,
"noEmit": true,
"noEmit": false,
"emitDeclarationOnly": true,
"outDir": "ts-dist",
"isolatedModules": true,
"paths": {
"@/*": ["./src/*"]

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.0.137",
"version": "1.0.138",
"private": true,
"type": "module",
"scripts": {

View File

@@ -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 { iife } from "@opencode-ai/util/iife"
import { Identifier } from "@opencode-ai/util/identifier"

View File

@@ -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 { SessionReview } from "@opencode-ai/ui/session-review"
import { DataProvider } from "@opencode-ai/ui/context"

View File

@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.0.137"
version = "1.0.138"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/sst/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.137/opencode-darwin-arm64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.138/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.137/opencode-darwin-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.138/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[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.138/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.137/opencode-linux-x64.tar.gz"
archive = "https://github.com/sst/opencode/releases/download/v1.0.138/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.137/opencode-windows-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.138/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.0.137",
"version": "1.0.138",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.0.137",
"version": "1.0.138",
"name": "opencode",
"type": "module",
"private": true,

View File

@@ -90,7 +90,7 @@ if (!Script.preview) {
"license=('MIT')",
"provides=('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")`,
`sha256sums_aarch64=('${arm64Sha}')`,
@@ -120,7 +120,7 @@ if (!Script.preview) {
"license=('MIT')",
"provides=('opencode')",
"conflicts=('opencode-bin')",
"depends=('fzf' 'ripgrep')",
"depends=('ripgrep')",
"makedepends=('git' 'bun-bin' 'go')",
"",
`source=("opencode-\${pkgver}.tar.gz::https://github.com/sst/opencode/archive/v\${pkgver}\${_subver}.tar.gz")`,

View File

@@ -25,7 +25,6 @@ import { Provider } from "../provider/provider"
import { Installation } from "@/installation"
import { MessageV2 } from "@/session/message-v2"
import { Config } from "@/config/config"
import { MCP } from "@/mcp"
import { Todo } from "@/session/todo"
import { z } from "zod"
import { LoadAPIKeyError } from "ai"

View File

@@ -245,7 +245,12 @@ export namespace Agent {
system.push(PROMPT_GENERATE)
const existing = await list()
const result = await generateObject({
experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry },
experimental_telemetry: {
isEnabled: cfg.experimental?.openTelemetry,
metadata: {
userId: cfg.username ?? "unknown",
},
},
temperature: 0.3,
prompt: [
...system.map(

View File

@@ -127,7 +127,18 @@ export namespace BunProc {
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))
return mod
}

View File

@@ -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",
})
}
}

View File

@@ -3,7 +3,7 @@ import { EventEmitter } from "events"
export const GlobalBus = new EventEmitter<{
event: [
{
directory: string
directory?: string
payload: any
},
]

View File

@@ -1,7 +1,7 @@
import z from "zod"
import type { ZodType } from "zod"
import { Log } from "../util/log"
import { Instance } from "../project/instance"
import { BusEvent } from "./bus-event"
import { GlobalBus } from "./global"
export namespace Bus {
@@ -9,10 +9,6 @@ export namespace Bus {
type Subscription = (event: any) => void
const disposedEventType = "server.instance.disposed"
export type EventDefinition = ReturnType<typeof event>
const registry = new Map<string, EventDefinition>()
const state = Instance.state(
() => {
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) {
const result = {
type,
properties,
}
registry.set(type, result)
return result
}
export const InstanceDisposed = event(
export const InstanceDisposed = BusEvent.define(
disposedEventType,
z.object({
directory: z.string(),
}),
)
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",
})
}
export async function publish<Definition extends EventDefinition>(
export async function publish<Definition extends BusEvent.Definition>(
def: Definition,
properties: z.output<Definition["properties"]>,
) {
@@ -100,14 +64,14 @@ export namespace Bus {
return Promise.all(pending)
}
export function subscribe<Definition extends EventDefinition>(
export function subscribe<Definition extends BusEvent.Definition>(
def: Definition,
callback: (event: { type: Definition["type"]; properties: z.infer<Definition["properties"]> }) => void,
) {
return raw(def.type, callback)
}
export function once<Definition extends EventDefinition>(
export function once<Definition extends BusEvent.Definition>(
def: Definition,
callback: (event: {
type: Definition["type"]

View File

@@ -813,10 +813,10 @@ export function Prompt(props: PromptProps) {
</text>
<Show when={store.mode === "normal"}>
<box flexDirection="row" gap={1}>
<text fg={theme.textMuted}>{local.model.parsed().provider}</text>
<text flexShrink={0} fg={theme.text}>
{local.model.parsed().model}
</text>
<text fg={theme.textMuted}>{local.model.parsed().provider}</text>
</box>
</Show>
</box>

View File

@@ -1,9 +1,10 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import z from "zod"
export const TuiEvent = {
PromptAppend: Bus.event("tui.prompt.append", z.object({ text: z.string() })),
CommandExecute: Bus.event(
PromptAppend: BusEvent.define("tui.prompt.append", z.object({ text: z.string() })),
CommandExecute: BusEvent.define(
"tui.command.execute",
z.object({
command: z.union([
@@ -27,7 +28,7 @@ export const TuiEvent = {
]),
}),
),
ToastShow: Bus.event(
ToastShow: BusEvent.define(
"tui.toast.show",
z.object({
title: z.string().optional(),

View File

@@ -1,14 +1,15 @@
import { Bus } from "@/bus"
import { BusEvent } from "@/bus/bus-event"
import z from "zod"
import { Config } from "../config/config"
import { Instance } from "../project/instance"
import { Bus } from "../bus"
import { Identifier } from "../id/id"
import PROMPT_INITIALIZE from "./template/initialize.txt"
import PROMPT_REVIEW from "./template/review.txt"
export namespace Command {
export const Event = {
Executed: Bus.event(
Executed: BusEvent.define(
"command.executed",
z.object({
name: z.string(),

View File

@@ -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
}
}

View File

@@ -1,5 +1,6 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import z from "zod"
import { Bus } from "../bus"
import { $ } from "bun"
import type { BunFile } from "bun"
import { formatPatch, structuredPatch } from "diff"
@@ -111,7 +112,7 @@ export namespace File {
}
export const Event = {
Edited: Bus.event(
Edited: BusEvent.define(
"file.edited",
z.object({
file: z.string(),

View File

@@ -1,5 +1,6 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import z from "zod"
import { Bus } from "../bus"
import { Instance } from "../project/instance"
import { Log } from "../util/log"
import { FileIgnore } from "./ignore"
@@ -8,6 +9,7 @@ import { Config } from "../config/config"
import { createWrapper } from "@parcel/watcher/wrapper"
import { lazy } from "@/util/lazy"
import type ParcelWatcher from "@parcel/watcher"
import { $ } from "bun"
declare const OPENCODE_LIBC: string | undefined
@@ -15,7 +17,7 @@ export namespace FileWatcher {
const log = Log.create({ service: "file.watcher" })
export const Event = {
Updated: Bus.event(
Updated: BusEvent.define(
"file.watcher.updated",
z.object({
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)) {
subs.push(
await watcher().subscribe(vcsDir, subscribe, {

View File

@@ -11,8 +11,8 @@ export namespace Flag {
export const OPENCODE_ENABLE_EXPERIMENTAL_MODELS = truthy("OPENCODE_ENABLE_EXPERIMENTAL_MODELS")
export const OPENCODE_DISABLE_AUTOCOMPACT = truthy("OPENCODE_DISABLE_AUTOCOMPACT")
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"]
export const OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH = number("OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH")
export const OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT = number("OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT")
// Experimental
export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL")
@@ -25,4 +25,11 @@ export namespace Flag {
const value = process.env[key]?.toLowerCase()
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
}
}

View File

@@ -1,8 +1,9 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { spawn } from "bun"
import z from "zod"
import { NamedError } from "@opencode-ai/util/error"
import { Log } from "../util/log"
import { Bus } from "../bus"
const SUPPORTED_IDES = [
{ name: "Windsurf" as const, cmd: "windsurf" },
@@ -16,7 +17,7 @@ export namespace Ide {
const log = Log.create({ service: "ide" })
export const Event = {
Installed: Bus.event(
Installed: BusEvent.define(
"ide.installed",
z.object({
ide: z.string(),

View File

@@ -1,8 +1,9 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import path from "path"
import { $ } from "bun"
import z from "zod"
import { NamedError } from "@opencode-ai/util/error"
import { Bus } from "../bus"
import { Log } from "../util/log"
import { iife } from "@/util/iife"
@@ -17,13 +18,13 @@ export namespace Installation {
export type Method = Awaited<ReturnType<typeof method>>
export const Event = {
Updated: Bus.event(
Updated: BusEvent.define(
"installation.updated",
z.object({
version: z.string(),
}),
),
UpdateAvailable: Bus.event(
UpdateAvailable: BusEvent.define(
"installation.update-available",
z.object({
version: z.string(),

View File

@@ -1,9 +1,10 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import path from "path"
import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node"
import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"
import { Log } from "../util/log"
import { LANGUAGE_EXTENSIONS } from "./language"
import { Bus } from "../bus"
import z from "zod"
import type { LSPServer } from "./server"
import { NamedError } from "@opencode-ai/util/error"
@@ -25,7 +26,7 @@ export namespace LSPClient {
)
export const Event = {
Diagnostics: Bus.event(
Diagnostics: BusEvent.define(
"lsp.client.diagnostics",
z.object({
serverID: z.string(),

View File

@@ -1,3 +1,5 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { Log } from "../util/log"
import { LSPClient } from "./client"
import path from "path"
@@ -6,13 +8,12 @@ import z from "zod"
import { Config } from "../config/config"
import { spawn } from "child_process"
import { Instance } from "../project/instance"
import { Bus } from "../bus"
export namespace LSP {
const log = Log.create({ service: "lsp" })
export const Event = {
Updated: Bus.event("lsp.updated", z.object({})),
Updated: BusEvent.define("lsp.updated", z.object({})),
}
export const Range = z

View File

@@ -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 = {
id: "gopls",
root: async (file) => {

View File

@@ -189,14 +189,14 @@ export namespace MCP {
name: "StreamableHTTP",
transport: new StreamableHTTPClientTransport(new URL(mcp.url), {
authProvider,
requestInit: oauthDisabled && mcp.headers ? { headers: mcp.headers } : undefined,
requestInit: mcp.headers ? { headers: mcp.headers } : undefined,
}),
},
{
name: "SSE",
transport: new SSEClientTransport(new URL(mcp.url), {
authProvider,
requestInit: oauthDisabled && mcp.headers ? { headers: mcp.headers } : undefined,
requestInit: mcp.headers ? { headers: mcp.headers } : undefined,
}),
},
]

View File

@@ -1,5 +1,6 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import z from "zod"
import { Bus } from "../bus"
import { Log } from "../util/log"
import { Identifier } from "../id/id"
import { Plugin } from "../plugin"
@@ -38,8 +39,8 @@ export namespace Permission {
export type Info = z.infer<typeof Info>
export const Event = {
Updated: Bus.event("permission.updated", Info),
Replied: Bus.event(
Updated: BusEvent.define("permission.updated", Info),
Replied: BusEvent.define(
"permission.replied",
z.object({
sessionID: z.string(),

View File

@@ -7,6 +7,10 @@ import { Log } from "../util/log"
import { Flag } from "@/flag/flag"
import { Session } from "../session"
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 {
const log = Log.create({ service: "project" })
@@ -14,10 +18,17 @@ export namespace Project {
.object({
id: z.string(),
worktree: z.string(),
vcsDir: z.string().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({
created: z.number(),
updated: z.number(),
initialized: z.number().optional(),
}),
})
@@ -26,74 +37,123 @@ export namespace Project {
})
export type Info = z.infer<typeof Info>
export const Event = {
Updated: BusEvent.define("project.updated", Info),
}
export async function fromDirectory(directory: string) {
log.info("fromDirectory", { directory })
const matches = Filesystem.up({ targets: [".git"], start: directory })
const git = await matches.next().then((x) => x.value)
await matches.return()
if (!git) {
const project: Info = {
const { id, worktree, vcs } = await iife(async () => {
const matches = Filesystem.up({ targets: [".git"], start: directory })
const git = await matches.next().then((x) => x.value)
await matches.return()
if (git) {
let worktree = path.dirname(git)
let id = await Bun.file(path.join(git, "opencode"))
.text()
.then((x) => x.trim())
.catch(() => {})
if (!id) {
const roots = await $`git rev-list --max-parents=0 --all`
.quiet()
.nothrow()
.cwd(worktree)
.text()
.then((x) =>
x
.split("\n")
.filter(Boolean)
.map((x) => x.trim())
.toSorted(),
)
id = roots[0]
if (id) Bun.file(path.join(git, "opencode")).write(id)
}
if (!id)
return {
id: "global",
worktree,
vcs: "git",
}
worktree = await $`git rev-parse --show-toplevel`
.quiet()
.nothrow()
.cwd(worktree)
.text()
.then((x) => path.resolve(worktree, x.trim()))
return { id, worktree, vcs: "git" }
}
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,
vcs: vcs as Info["vcs"],
time: {
created: Date.now(),
updated: Date.now(),
},
}
await Storage.write<Info>(["project", "global"], project)
return project
if (id !== "global") {
await migrateFromGlobal(id, worktree)
}
}
let worktree = path.dirname(git)
const timer = log.time("git.rev-parse")
let id = await Bun.file(path.join(git, "opencode"))
.text()
.then((x) => x.trim())
.catch(() => {})
if (!id) {
const roots = await $`git rev-list --max-parents=0 --all`
.quiet()
.nothrow()
.cwd(worktree)
.text()
.then((x) =>
x
.split("\n")
.filter(Boolean)
.map((x) => x.trim())
.toSorted(),
)
id = roots[0]
if (id) Bun.file(path.join(git, "opencode")).write(id)
}
timer.stop()
worktree = await $`git rev-parse --show-toplevel`
.quiet()
.nothrow()
.cwd(worktree)
.text()
.then((x) => path.resolve(worktree, x.trim()))
const vcsDir = await $`git rev-parse --git-dir`
.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,
discover(existing)
const result: Info = {
...existing,
worktree,
vcsDir,
vcs: "git",
vcs: vcs as Info["vcs"],
time: {
created: Date.now(),
...existing.time,
updated: Date.now(),
},
}
await Storage.write<Info>(["project", projectID], project)
return project
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) 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) {
@@ -130,4 +190,19 @@ export namespace Project {
const keys = await Storage.list(["project"])
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) => {
return await Storage.update<Info>(["project", input.projectID], (draft) => {
if (input.name !== undefined) draft.name = input.name
if (input.icon !== undefined) draft.icon = input.icon
draft.time.updated = Date.now()
})
},
)
}

View File

@@ -1,8 +1,9 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { $ } from "bun"
import path from "path"
import z from "zod"
import { Log } from "@/util/log"
import { Bus } from "@/bus"
import { Instance } from "./instance"
import { FileWatcher } from "@/file/watcher"
@@ -10,7 +11,7 @@ const log = Log.create({ service: "vcs" })
export namespace Vcs {
export const Event = {
BranchUpdated: Bus.event(
BranchUpdated: BusEvent.define(
"vcs.branch.updated",
z.object({
branch: z.string().optional(),
@@ -39,16 +40,14 @@ export namespace Vcs {
const state = Instance.state(
async () => {
const vcsDir = Instance.project.vcsDir
if (Instance.project.vcs !== "git" || !vcsDir) {
if (Instance.project.vcs !== "git") {
return { branch: async () => undefined, unsubscribe: undefined }
}
let current = await currentBranch()
log.info("initialized", { branch: current })
const head = path.join(vcsDir, "HEAD")
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()
if (next !== current) {
log.info("branch changed", { from: current, to: next })

View File

@@ -17,6 +17,16 @@ export namespace ModelsDev {
reasoning: z.boolean(),
temperature: 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
.object({
input: z.number(),

View File

@@ -349,6 +349,12 @@ export namespace Provider {
video: z.boolean(),
pdf: z.boolean(),
}),
interleaved: z.union([
z.boolean(),
z.object({
field: z.enum(["reasoning_content", "reasoning_details"]),
}),
]),
}),
cost: z.object({
input: z.number(),
@@ -450,6 +456,7 @@ export namespace Provider {
video: model.modalities?.output?.includes("video") ?? false,
pdf: model.modalities?.output?.includes("pdf") ?? false,
},
interleaved: model.interleaved ?? false,
},
}
}
@@ -567,6 +574,7 @@ export namespace Provider {
video: model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false,
pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false,
},
interleaved: model.interleaved ?? false,
},
cost: {
input: model?.cost?.input ?? existingModel?.cost?.input ?? 0,

View File

@@ -273,8 +273,24 @@ export namespace ProviderTransform {
return options
}
export function providerOptions(npm: string | undefined, providerID: string, options: { [x: string]: any }) {
switch (npm) {
export function providerOptions(model: Provider.Model, options: { [x: string]: any }, messages: ModelMessage[]) {
if (model.capabilities.interleaved && typeof model.capabilities.interleaved === "object") {
const cot = []
const assistantMessages = messages.filter((msg) => msg.role === "assistant")
for (const msg of assistantMessages) {
for (const part of msg.content) {
if (typeof part === "string") {
continue
}
if (part.type === "reasoning") {
cot.push(part)
}
}
}
options[model.capabilities.interleaved.field] = cot
}
switch (model.api.npm) {
case "@ai-sdk/openai":
case "@ai-sdk/azure":
return {
@@ -302,7 +318,7 @@ export namespace ProviderTransform {
}
default:
return {
[providerID]: options,
[model.providerID]: options,
}
}
}

View File

@@ -1,8 +1,9 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { type IPty } from "bun-pty"
import z from "zod"
import { Identifier } from "../id/id"
import { Log } from "../util/log"
import { Bus } from "../bus"
import type { WSContext } from "hono/ws"
import { Instance } from "../project/instance"
import { shell } from "@opencode-ai/util/shell"
@@ -73,10 +74,10 @@ export namespace Pty {
export type UpdateInput = z.infer<typeof UpdateInput>
export const Event = {
Created: Bus.event("pty.created", z.object({ info: Info })),
Updated: Bus.event("pty.updated", z.object({ info: Info })),
Exited: Bus.event("pty.exited", z.object({ id: Identifier.schema("pty"), exitCode: z.number() })),
Deleted: Bus.event("pty.deleted", z.object({ id: Identifier.schema("pty") })),
Created: BusEvent.define("pty.created", z.object({ info: Info })),
Updated: BusEvent.define("pty.updated", z.object({ info: Info })),
Exited: BusEvent.define("pty.exited", z.object({ id: Identifier.schema("pty"), exitCode: z.number() })),
Deleted: BusEvent.define("pty.deleted", z.object({ id: Identifier.schema("pty") })),
}
interface ActiveSession {

View File

@@ -1,8 +1,10 @@
import { Hono } from "hono"
import { describeRoute } from "hono-openapi"
import { describeRoute, validator } from "hono-openapi"
import { resolver } from "hono-openapi"
import { Instance } from "../project/instance"
import { Project } from "../project/project"
import z from "zod"
import { errors } from "./error"
export const ProjectRoute = new Hono()
.get(
@@ -48,3 +50,30 @@ export const ProjectRoute = new Hono()
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)
},
)

View File

@@ -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 { Bus } from "../bus"
import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
import { Hono } from "hono"
import { cors } from "hono/cors"
@@ -8,7 +10,7 @@ import { proxy } from "hono/proxy"
import { Session } from "../session"
import z from "zod"
import { Provider } from "../provider/provider"
import { mapValues, pipe } from "remeda"
import { mapValues } from "remeda"
import { NamedError } from "@opencode-ai/util/error"
import { ModelsDev } from "../provider/models"
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 { Snapshot } from "@/snapshot"
import { SessionSummary } from "@/session/summary"
import { GlobalBus } from "@/bus/global"
import { SessionStatus } from "@/session/status"
import { upgradeWebSocket, websocket } from "hono/bun"
import { errors } from "./error"
@@ -54,7 +55,7 @@ export namespace Server {
const log = Log.create({ service: "server" })
export const Event = {
Connected: Bus.event("server.connected", z.object({})),
Connected: BusEvent.define("server.connected", z.object({})),
}
const app = new Hono()
@@ -109,7 +110,7 @@ export namespace Server {
z
.object({
directory: z.string(),
payload: Bus.payloads(),
payload: BusEvent.payloads(),
})
.meta({
ref: "GlobalEvent",
@@ -2393,7 +2394,7 @@ export namespace Server {
description: "Event stream",
content: {
"text/event-stream": {
schema: resolver(Bus.payloads()),
schema: resolver(BusEvent.payloads()),
},
},
},

View File

@@ -1,3 +1,5 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { wrapLanguageModel, type ModelMessage } from "ai"
import { Session } from "."
import { Identifier } from "../id/id"
@@ -5,7 +7,6 @@ import { Instance } from "../project/instance"
import { Provider } from "../provider/provider"
import { MessageV2 } from "./message-v2"
import { SystemPrompt } from "./system"
import { Bus } from "../bus"
import z from "zod"
import { SessionPrompt } from "./prompt"
import { Flag } from "../flag/flag"
@@ -21,7 +22,7 @@ export namespace SessionCompaction {
const log = Log.create({ service: "session.compaction" })
export const Event = {
Compacted: Bus.event(
Compacted: BusEvent.define(
"session.compacted",
z.object({
sessionID: z.string(),
@@ -140,9 +141,9 @@ export namespace SessionCompaction {
// set to 0, we handle loop
maxRetries: 0,
providerOptions: ProviderTransform.providerOptions(
model.api.npm,
model.providerID,
model,
pipe({}, mergeDeep(ProviderTransform.options(model, input.sessionID)), mergeDeep(model.options)),
[],
),
headers: model.headers,
abortSignal: input.abort,
@@ -193,7 +194,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) {
const continueMsg = await Session.updateMessage({

View File

@@ -1,7 +1,8 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { Decimal } from "decimal.js"
import z from "zod"
import { type LanguageModelUsage, type ProviderMetadata } from "ai"
import { Bus } from "../bus"
import { Config } from "../config/config"
import { Flag } from "../flag/flag"
import { Identifier } from "../id/id"
@@ -85,32 +86,32 @@ export namespace Session {
export type ShareInfo = z.output<typeof ShareInfo>
export const Event = {
Created: Bus.event(
Created: BusEvent.define(
"session.created",
z.object({
info: Info,
}),
),
Updated: Bus.event(
Updated: BusEvent.define(
"session.updated",
z.object({
info: Info,
}),
),
Deleted: Bus.event(
Deleted: BusEvent.define(
"session.deleted",
z.object({
info: Info,
}),
),
Diff: Bus.event(
Diff: BusEvent.define(
"session.diff",
z.object({
sessionID: z.string(),
diff: Snapshot.FileDiff.array(),
}),
),
Error: Bus.event(
Error: BusEvent.define(
"session.error",
z.object({
sessionID: z.string().optional(),

View File

@@ -1,5 +1,6 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import z from "zod"
import { Bus } from "../bus"
import { NamedError } from "@opencode-ai/util/error"
import { Message } from "./message"
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 const Event = {
Updated: Bus.event(
Updated: BusEvent.define(
"message.updated",
z.object({
info: Info,
}),
),
Removed: Bus.event(
Removed: BusEvent.define(
"message.removed",
z.object({
sessionID: z.string(),
messageID: z.string(),
}),
),
PartUpdated: Bus.event(
PartUpdated: BusEvent.define(
"message.part.updated",
z.object({
part: Part,
delta: z.string().optional(),
}),
),
PartRemoved: Bus.event(
PartRemoved: BusEvent.define(
"message.part.removed",
z.object({
sessionID: z.string(),

View File

@@ -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({
onError(error) {
log.error("stream error", {
@@ -562,42 +593,12 @@ export namespace SessionPrompt {
OUTPUT_TOKEN_MAX,
),
abortSignal: abort,
providerOptions: ProviderTransform.providerOptions(model.api.npm, model.providerID, params.options),
providerOptions: ProviderTransform.providerOptions(model, params.options, messages),
stopWhen: stepCountIs(1),
temperature: params.temperature,
topP: params.topP,
toolChoice: isLastStep ? "none" : undefined,
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,
},
]
: []),
],
messages,
tools: model.capabilities.toolcall === false ? undefined : tools,
model: wrapLanguageModel({
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
continue
@@ -1458,7 +1465,7 @@ export namespace SessionPrompt {
await generateText({
// use higher # for reasoning models since reasoning tokens eat up a lot of the budget
maxOutputTokens: small.capabilities.reasoning ? 3000 : 20,
providerOptions: ProviderTransform.providerOptions(small.api.npm, small.providerID, options),
providerOptions: ProviderTransform.providerOptions(small, options, []),
messages: [
...SystemPrompt.title(small.providerID).map(
(x): ModelMessage => ({
@@ -1491,7 +1498,13 @@ export namespace SessionPrompt {
],
headers: small.headers,
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) => {
if (result.text)

View File

@@ -1,3 +1,4 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { Instance } from "@/project/instance"
import z from "zod"
@@ -24,7 +25,7 @@ export namespace SessionStatus {
export type Info = z.infer<typeof Info>
export const Event = {
Status: Bus.event(
Status: BusEvent.define(
"session.status",
z.object({
sessionID: z.string(),
@@ -32,7 +33,7 @@ export namespace SessionStatus {
}),
),
// deprecated
Idle: Bus.event(
Idle: BusEvent.define(
"session.idle",
z.object({
sessionID: z.string(),

View File

@@ -91,7 +91,7 @@ export namespace SessionSummary {
if (textPart && !userMsg.summary?.title) {
const result = await generateText({
maxOutputTokens: small.capabilities.reasoning ? 1500 : 20,
providerOptions: ProviderTransform.providerOptions(small.api.npm, small.providerID, options),
providerOptions: ProviderTransform.providerOptions(small, options, []),
messages: [
...SystemPrompt.title(small.providerID).map(
(x): ModelMessage => ({
@@ -111,7 +111,13 @@ export namespace SessionSummary {
],
headers: small.headers,
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 })
userMsg.summary.title = result.text
@@ -138,7 +144,7 @@ export namespace SessionSummary {
const result = await generateText({
model: language,
maxOutputTokens: 100,
providerOptions: ProviderTransform.providerOptions(small.api.npm, small.providerID, options),
providerOptions: ProviderTransform.providerOptions(small, options, []),
messages: [
...SystemPrompt.summarize(small.providerID).map(
(x): ModelMessage => ({
@@ -153,7 +159,13 @@ export namespace SessionSummary {
},
],
headers: small.headers,
experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry },
experimental_telemetry: {
isEnabled: cfg.experimental?.openTelemetry,
metadata: {
userId: cfg.username ?? "unknown",
sessionId: assistantMsg.sessionID,
},
},
}).catch(() => {})
if (result) summary = result.text
}

View File

@@ -1,5 +1,6 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import z from "zod"
import { Bus } from "../bus"
import { Storage } from "../storage/storage"
export namespace Todo {
@@ -14,7 +15,7 @@ export namespace Todo {
export type Info = z.infer<typeof Info>
export const Event = {
Updated: Bus.event(
Updated: BusEvent.define(
"todo.updated",
z.object({
sessionID: z.string(),

View File

@@ -16,12 +16,8 @@ import { Flag } from "@/flag/flag.ts"
import path from "path"
import { iife } from "@/util/iife"
const DEFAULT_MAX_OUTPUT_LENGTH = 30_000
const MAX_OUTPUT_LENGTH = (() => {
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 MAX_OUTPUT_LENGTH = Flag.OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH || 30_000
const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT || 2 * 60 * 1000
const SIGKILL_TIMEOUT_MS = 200
export const log = Log.create({ service: "bash-tool" })

View File

@@ -93,7 +93,7 @@ export const ReadTool = Tool.define("read", {
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"
if (isImage || isPdf) {
const mime = file.type

View File

@@ -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()
}
},
})
})

View File

@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test"
import { Project } from "../../src/project/project"
import { Log } from "../../src/util/log"
import { Storage } from "../../src/storage/storage"
import { $ } from "bun"
import path from "path"
import { tmpdir } from "../fixture/fixture"
@@ -18,7 +19,6 @@ describe("Project.fromDirectory", () => {
expect(project.id).toBe("global")
expect(project.vcs).toBe("git")
expect(project.worktree).toBe(tmp.path)
expect(project.vcsDir).toBe(path.join(tmp.path, ".git"))
const opencodeFile = path.join(tmp.path, ".git", "opencode")
const fileExists = await Bun.file(opencodeFile).exists()
@@ -40,3 +40,33 @@ describe("Project.fromDirectory", () => {
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()
})
})

View File

@@ -130,6 +130,7 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => {
toolcall: true,
input: { text: true, audio: false, image: false, video: false, pdf: false },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: false,
},
cost: {
input: 0.001,
@@ -184,6 +185,7 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => {
toolcall: true,
input: { text: true, audio: false, image: false, video: false, pdf: false },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: false,
},
cost: {
input: 0.001,
@@ -236,6 +238,7 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => {
toolcall: true,
input: { text: true, audio: false, image: false, video: false, pdf: false },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: false,
},
cost: {
input: 0.001,
@@ -281,6 +284,7 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => {
toolcall: true,
input: { text: true, audio: false, image: true, video: false, pdf: false },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: false,
},
cost: {
input: 0.03,

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.0.137",
"version": "1.0.138",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.0.137",
"version": "1.0.138",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",

View File

@@ -51,6 +51,8 @@ import type {
PermissionRespondResponses,
ProjectCurrentResponses,
ProjectListResponses,
ProjectUpdateErrors,
ProjectUpdateResponses,
ProviderAuthResponses,
ProviderListResponses,
ProviderOauthAuthorizeErrors,
@@ -231,6 +233,48 @@ export class Project extends HeyApiClient {
...params,
})
}
/**
* Update project
*
* Update project properties such as name, icon and color.
*/
public update<ThrowOnError extends boolean = false>(
parameters: {
projectID: string
directory?: string
name?: string
icon?: {
url?: string
color?: string
}
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "projectID" },
{ in: "query", key: "directory" },
{ in: "body", key: "name" },
{ in: "body", key: "icon" },
],
},
],
)
return (options?.client ?? this.client).patch<ProjectUpdateResponses, ProjectUpdateErrors, ThrowOnError>({
url: "/project/{projectID}",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
}
export class Pty extends HeyApiClient {

View File

@@ -4,13 +4,6 @@ export type ClientOptions = {
baseUrl: `${string}://${string}` | (string & {})
}
export type EventServerInstanceDisposed = {
type: "server.instance.disposed"
properties: {
directory: string
}
}
export type EventInstallationUpdated = {
type: "installation.updated"
properties: {
@@ -25,6 +18,34 @@ export type EventInstallationUpdateAvailable = {
}
}
export type Project = {
id: string
worktree: string
vcs?: "git"
name?: string
icon?: {
url?: string
color?: string
}
time: {
created: number
updated: number
initialized?: number
}
}
export type EventProjectUpdated = {
type: "project.updated"
properties: Project
}
export type EventServerInstanceDisposed = {
type: "server.instance.disposed"
properties: {
directory: string
}
}
export type EventLspClientDiagnostics = {
type: "lsp.client.diagnostics"
properties: {
@@ -704,9 +725,10 @@ export type EventServerConnected = {
}
export type Event =
| EventServerInstanceDisposed
| EventInstallationUpdated
| EventInstallationUpdateAvailable
| EventProjectUpdated
| EventServerInstanceDisposed
| EventLspClientDiagnostics
| EventLspUpdated
| EventMessageUpdated
@@ -742,17 +764,6 @@ export type GlobalEvent = {
payload: Event
}
export type Project = {
id: string
worktree: string
vcsDir?: string
vcs?: "git"
time: {
created: number
initialized?: number
}
}
export type BadRequestError = {
data: unknown
errors: Array<{
@@ -1033,6 +1044,11 @@ export type ProviderConfig = {
reasoning?: boolean
temperature?: boolean
tool_call?: boolean
interleaved?:
| true
| {
field: "reasoning_content" | "reasoning_details"
}
cost?: {
input: number
output: number
@@ -1468,6 +1484,11 @@ export type Model = {
video: boolean
pdf: boolean
}
interleaved:
| boolean
| {
field: "reasoning_content" | "reasoning_details"
}
}
cost: {
input: number
@@ -1715,6 +1736,45 @@ export type ProjectCurrentResponses = {
export type ProjectCurrentResponse = ProjectCurrentResponses[keyof ProjectCurrentResponses]
export type ProjectUpdateData = {
body?: {
name?: string
icon?: {
url?: string
color?: string
}
}
path: {
projectID: string
}
query?: {
directory?: string
}
url: "/project/{projectID}"
}
export type ProjectUpdateErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type ProjectUpdateError = ProjectUpdateErrors[keyof ProjectUpdateErrors]
export type ProjectUpdateResponses = {
/**
* Updated project information
*/
200: Project
}
export type ProjectUpdateResponse = ProjectUpdateResponses[keyof ProjectUpdateResponses]
export type PtyListData = {
body?: never
path?: never
@@ -2976,6 +3036,11 @@ export type ProviderListResponses = {
reasoning: boolean
temperature: boolean
tool_call: boolean
interleaved?:
| true
| {
field: "reasoning_content" | "reasoning_details"
}
cost?: {
input: number
output: number

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.0.137",
"version": "1.0.138",
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",

View File

@@ -1,12 +1,12 @@
{
"name": "@opencode-ai/tauri",
"private": true,
"version": "1.0.137",
"version": "1.0.138",
"type": "module",
"scripts": {
"predev": "bun ./scripts/predev.ts",
"dev": "vite",
"build": "tsc && vite build",
"build": "bun run typecheck && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
@@ -15,6 +15,7 @@
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-process": "~2",
"@tauri-apps/plugin-shell": "~2",
"@tauri-apps/plugin-updater": "~2",
"solid-js": "catalog:"
@@ -23,6 +24,7 @@
"@actions/artifact": "4.0.0",
"@tauri-apps/cli": "^2",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
"typescript": "~5.6.2",
"vite": "catalog:"
}

View File

@@ -2511,6 +2511,7 @@ dependencies = [
"tauri-build",
"tauri-plugin-dialog",
"tauri-plugin-opener",
"tauri-plugin-process",
"tauri-plugin-shell",
"tauri-plugin-updater",
"tokio",
@@ -4143,6 +4144,16 @@ dependencies = [
"zbus",
]
[[package]]
name = "tauri-plugin-process"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a"
dependencies = [
"tauri",
"tauri-plugin",
]
[[package]]
name = "tauri-plugin-shell"
version = "2.3.3"

View File

@@ -28,3 +28,4 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = "1.48.0"
listeners = "0.3"
tauri-plugin-process = "2"

View File

@@ -9,6 +9,7 @@
"core:window:allow-start-dragging",
"shell:default",
"updater:default",
"dialog:default"
"dialog:default",
"process:default"
]
}

View File

@@ -8,7 +8,6 @@ use tauri::{AppHandle, Manager, RunEvent, WebviewUrl, WebviewWindow};
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
use tauri_plugin_shell::ShellExt;
use tauri_plugin_updater::UpdaterExt;
use tokio::net::TcpSocket;
#[derive(Clone)]
@@ -102,14 +101,11 @@ pub fn run() {
let mut builder = tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_opener::init())
.setup(move |app| {
let app = app.handle().clone();
if updater_enabled {
tauri::async_runtime::spawn(run_updater(app.clone()));
}
tauri::async_runtime::spawn(async move {
let port = get_sidecar_port();
let socket_connected = is_server_running(port).await;
@@ -141,7 +137,7 @@ pub fn run() {
let timestamp = Instant::now();
loop {
if timestamp.elapsed() > Duration::from_secs(3) {
if timestamp.elapsed() > Duration::from_secs(7) {
todo!("Handle server spawn timeout");
}
@@ -167,7 +163,13 @@ pub fn run() {
.title("OpenCode")
.inner_size(800.0, 600.0)
.decorations(true)
.zoom_hotkeys_enabled(true);
.zoom_hotkeys_enabled(true)
.initialization_script(format!(
r#"
window.__OPENCODE__ ??= {{}};
window.__OPENCODE__.updaterEnabled = {updater_enabled}
"#
));
#[cfg(target_os = "macos")]
{
@@ -193,78 +195,24 @@ pub fn run() {
if let RunEvent::Exit = event {
println!("Received Exit");
let _ = app
.state::<ServerState>()
let Some(server_state) = app.try_state::<ServerState>() else {
println!("Server not running");
return;
};
let Some(server_state) = server_state
.0
.lock()
.expect("Failed to acquire mutex lock")
.take()
.expect("State not found")
.kill();
else {
println!("Server state missing");
return;
};
let _ = server_state.kill();
println!("Killed server");
}
});
}
async fn run_updater(app: AppHandle) {
let update = match app
.updater_builder()
.version_comparator(|v, r| {
dbg!(&v, &r);
r.version > v
})
.build()
.unwrap()
.check()
.await
{
Ok(u) => u,
Err(e) => {
dbg!(e);
app.dialog()
.message("Failed to check for updates")
.show(|_| {});
return;
}
};
dbg!(update.is_some());
let Some(update) = update else {
return;
};
let Ok(update_bytes) = update.download(|_, _| {}, || {}).await else {
return;
};
let should_update = app
.dialog()
.message(format!(
"Version {} of OpenCode is available, would you like to install it?",
&update.version
))
.buttons(MessageDialogButtons::YesNo)
.blocking_show();
if !should_update {
return;
}
if update.install(update_bytes).is_err() {
app.dialog()
.message("Failed to install update")
.blocking_show();
}
let should_restart = app
.dialog()
.message("Update installed successfully, would you like to restart OpenCode?")
.buttons(MessageDialogButtons::YesNo)
.blocking_show();
if should_restart {
app.restart();
}
}

View File

@@ -1,6 +1,10 @@
// @refresh reload
import { render } from "solid-js/web"
import { DesktopInterface, PlatformProvider, Platform } from "@opencode-ai/desktop"
import { App, PlatformProvider, Platform } from "@opencode-ai/desktop"
import { runUpdater } from "./updater"
import { onMount } from "solid-js"
import { open, save } from "@tauri-apps/plugin-dialog"
import { open as shellOpen } from "@tauri-apps/plugin-shell"
const root = document.getElementById("root")
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
@@ -9,13 +13,54 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
)
}
const platform: Platform = {}
const platform: Platform = {
platform: "tauri",
render(
() => (
async openDirectoryPickerDialog(opts) {
const result = await open({
directory: true,
multiple: opts?.multiple ?? false,
title: opts?.title ?? "Choose a folder",
})
return result
},
async openFilePickerDialog(opts) {
const result = await open({
directory: false,
multiple: opts?.multiple ?? false,
title: opts?.title ?? "Choose a file",
})
return result
},
async saveFilePickerDialog(opts) {
const result = await save({
title: opts?.title ?? "Save file",
defaultPath: opts?.defaultPath,
})
return result
},
openLink(url: string) {
shellOpen(url)
},
}
declare global {
interface Window {
__OPENCODE__?: { updaterEnabled?: boolean }
}
}
render(() => {
onMount(() => {
if (window.__OPENCODE__?.updaterEnabled) runUpdater()
})
return (
<PlatformProvider value={platform}>
<DesktopInterface />
<App />
</PlatformProvider>
),
root!,
)
)
}, root!)

View File

@@ -0,0 +1,37 @@
import { check, DownloadEvent } from "@tauri-apps/plugin-updater"
import { relaunch } from "@tauri-apps/plugin-process"
import { ask, message } from "@tauri-apps/plugin-dialog"
export async function runUpdater(onDownloadEvent?: (progress: DownloadEvent) => void) {
let update
try {
update = await check()
} catch {
await message("Failed to check for updates")
return false
}
if (!update) return
if (update.version <= update.currentVersion) return
try {
await update.download(onDownloadEvent)
} catch {
return false
}
const shouldUpdate = await ask(`Version ${update.version} of OpenCode is available, would you like to install it?`)
if (!shouldUpdate) return
try {
await update.install()
} catch {
await message("Failed to install update")
return false
}
const shouldRestart = await ask("Update installed successfully, would you like to restart OpenCode?")
if (shouldRestart) await relaunch()
return true
}

View File

@@ -1,26 +1,8 @@
{
"extends": "../desktop/tsconfig.json",
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"jsx": "preserve",
"jsxImportSource": "solid-js"
"outDir": "ts-dist"
},
"references": [{ "path": "../desktop" }],
"include": ["src"]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.0.137",
"version": "1.0.138",
"type": "module",
"exports": {
"./*": "./src/components/*.tsx",

View File

@@ -0,0 +1,40 @@
[data-component="avatar"] {
--avatar-bg: var(--color-surface-info-base);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
border-radius: var(--radius-sm);
border: 1px solid var(--color-border-weak-base);
font-family: var(--font-mono);
font-weight: 500;
text-transform: uppercase;
background-color: var(--avatar-bg);
color: oklch(from var(--avatar-bg) calc(l * 0.72) calc(c * 8) h);
}
[data-component="avatar"][data-has-image] {
background-color: transparent;
border: none;
}
[data-component="avatar"][data-size="small"] {
width: 1.25rem;
height: 1.25rem;
font-size: 0.75rem;
line-height: 1;
}
[data-component="avatar"][data-size="normal"] {
width: 1.5rem;
height: 1.5rem;
font-size: 1.125rem;
line-height: 1.5rem;
}
[data-component="avatar"][data-size="large"] {
width: 2rem;
height: 2rem;
font-size: 1.25rem;
line-height: 2rem;
}

View File

@@ -0,0 +1,32 @@
import { type ComponentProps, splitProps, Show } from "solid-js"
export interface AvatarProps extends ComponentProps<"div"> {
fallback: string
src?: string
background?: string
size?: "small" | "normal" | "large"
}
export function Avatar(props: AvatarProps) {
const [split, rest] = splitProps(props, ["fallback", "src", "background", "size", "class", "classList", "style"])
return (
<div
{...rest}
data-component="avatar"
data-size={split.size || "normal"}
data-has-image={split.src ? "" : undefined}
classList={{
...(split.classList ?? {}),
[split.class ?? ""]: !!split.class,
}}
style={{
...(typeof split.style === "object" ? split.style : {}),
...(!split.src && split.background ? { "--avatar-bg": split.background } : {}),
}}
>
<Show when={split.src} fallback={split.fallback?.[0]}>
{(src) => <img src={src()} draggable={false} class="size-full object-cover" />}
</Show>
</div>
)
}

View File

@@ -4,7 +4,7 @@
justify-content: center;
border-style: solid;
border-width: 1px;
border-radius: var(--radius-sm);
border-radius: var(--radius-md);
text-decoration: none;
user-select: none;
cursor: default;
@@ -54,6 +54,12 @@
opacity: 0.7;
cursor: not-allowed;
}
&[data-selected="true"]:not(:disabled) {
background-color: var(--surface-raised-base-hover);
}
&[data-active="true"] {
background-color: var(--surface-raised-base-active);
}
}
&[data-variant="secondary"] {

View File

@@ -0,0 +1,119 @@
[data-component="dropdown-menu-content"],
[data-component="dropdown-menu-sub-content"] {
min-width: 8rem;
overflow: hidden;
border-radius: var(--radius-md);
border: 1px solid var(--border-weak-base);
background-color: var(--surface-raised-stronger-non-alpha);
padding: 4px;
box-shadow: var(--shadow-md);
z-index: 50;
transform-origin: var(--kb-menu-content-transform-origin);
&[data-closed] {
animation: dropdown-menu-close 0.15s ease-out;
}
&[data-expanded] {
animation: dropdown-menu-open 0.15s ease-out;
}
}
[data-component="dropdown-menu-content"],
[data-component="dropdown-menu-sub-content"] {
[data-slot="dropdown-menu-item"],
[data-slot="dropdown-menu-checkbox-item"],
[data-slot="dropdown-menu-radio-item"],
[data-slot="dropdown-menu-sub-trigger"] {
position: relative;
display: flex;
align-items: center;
gap: 8px;
padding: 4px 8px;
border-radius: var(--radius-sm);
cursor: default;
user-select: none;
outline: none;
font-family: var(--font-family-sans);
font-size: var(--font-size-small);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large);
letter-spacing: var(--letter-spacing-normal);
color: var(--text-strong);
&[data-highlighted] {
background: var(--surface-raised-base-hover);
}
&[data-disabled] {
color: var(--text-weak);
pointer-events: none;
}
}
[data-slot="dropdown-menu-sub-trigger"] {
&[data-expanded] {
background: var(--surface-raised-base-hover);
}
}
[data-slot="dropdown-menu-item-indicator"] {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
}
[data-slot="dropdown-menu-item-label"] {
flex: 1;
}
[data-slot="dropdown-menu-item-description"] {
font-size: var(--font-size-x-small);
color: var(--text-weak);
}
[data-slot="dropdown-menu-separator"] {
height: 1px;
margin: 4px -4px;
border-top-color: var(--border-weak-base);
}
[data-slot="dropdown-menu-group-label"] {
padding: 4px 8px;
font-family: var(--font-family-sans);
font-size: var(--font-size-x-small);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large);
letter-spacing: var(--letter-spacing-normal);
color: var(--text-weak);
}
[data-slot="dropdown-menu-arrow"] {
fill: var(--surface-raised-stronger-non-alpha);
}
}
@keyframes dropdown-menu-open {
from {
opacity: 0;
transform: scale(0.96);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes dropdown-menu-close {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.96);
}
}

View File

@@ -0,0 +1,308 @@
import { DropdownMenu as Kobalte } from "@kobalte/core/dropdown-menu"
import { splitProps } from "solid-js"
import type { ComponentProps, ParentProps } from "solid-js"
export interface DropdownMenuProps extends ComponentProps<typeof Kobalte> {}
export interface DropdownMenuTriggerProps extends ComponentProps<typeof Kobalte.Trigger> {}
export interface DropdownMenuIconProps extends ComponentProps<typeof Kobalte.Icon> {}
export interface DropdownMenuPortalProps extends ComponentProps<typeof Kobalte.Portal> {}
export interface DropdownMenuContentProps extends ComponentProps<typeof Kobalte.Content> {}
export interface DropdownMenuArrowProps extends ComponentProps<typeof Kobalte.Arrow> {}
export interface DropdownMenuSeparatorProps extends ComponentProps<typeof Kobalte.Separator> {}
export interface DropdownMenuGroupProps extends ComponentProps<typeof Kobalte.Group> {}
export interface DropdownMenuGroupLabelProps extends ComponentProps<typeof Kobalte.GroupLabel> {}
export interface DropdownMenuItemProps extends ComponentProps<typeof Kobalte.Item> {}
export interface DropdownMenuItemLabelProps extends ComponentProps<typeof Kobalte.ItemLabel> {}
export interface DropdownMenuItemDescriptionProps extends ComponentProps<typeof Kobalte.ItemDescription> {}
export interface DropdownMenuItemIndicatorProps extends ComponentProps<typeof Kobalte.ItemIndicator> {}
export interface DropdownMenuRadioGroupProps extends ComponentProps<typeof Kobalte.RadioGroup> {}
export interface DropdownMenuRadioItemProps extends ComponentProps<typeof Kobalte.RadioItem> {}
export interface DropdownMenuCheckboxItemProps extends ComponentProps<typeof Kobalte.CheckboxItem> {}
export interface DropdownMenuSubProps extends ComponentProps<typeof Kobalte.Sub> {}
export interface DropdownMenuSubTriggerProps extends ComponentProps<typeof Kobalte.SubTrigger> {}
export interface DropdownMenuSubContentProps extends ComponentProps<typeof Kobalte.SubContent> {}
function DropdownMenuRoot(props: DropdownMenuProps) {
return <Kobalte {...props} data-component="dropdown-menu" />
}
function DropdownMenuTrigger(props: ParentProps<DropdownMenuTriggerProps>) {
const [local, rest] = splitProps(props, ["class", "classList", "children"])
return (
<Kobalte.Trigger
{...rest}
data-slot="dropdown-menu-trigger"
classList={{
...(local.classList ?? {}),
[local.class ?? ""]: !!local.class,
}}
>
{local.children}
</Kobalte.Trigger>
)
}
function DropdownMenuIcon(props: ParentProps<DropdownMenuIconProps>) {
const [local, rest] = splitProps(props, ["class", "classList", "children"])
return (
<Kobalte.Icon
{...rest}
data-slot="dropdown-menu-icon"
classList={{
...(local.classList ?? {}),
[local.class ?? ""]: !!local.class,
}}
>
{local.children}
</Kobalte.Icon>
)
}
function DropdownMenuPortal(props: DropdownMenuPortalProps) {
return <Kobalte.Portal {...props} />
}
function DropdownMenuContent(props: ParentProps<DropdownMenuContentProps>) {
const [local, rest] = splitProps(props, ["class", "classList", "children"])
return (
<Kobalte.Content
{...rest}
data-component="dropdown-menu-content"
classList={{
...(local.classList ?? {}),
[local.class ?? ""]: !!local.class,
}}
>
{local.children}
</Kobalte.Content>
)
}
function DropdownMenuArrow(props: DropdownMenuArrowProps) {
const [local, rest] = splitProps(props, ["class", "classList"])
return (
<Kobalte.Arrow
{...rest}
data-slot="dropdown-menu-arrow"
classList={{
...(local.classList ?? {}),
[local.class ?? ""]: !!local.class,
}}
/>
)
}
function DropdownMenuSeparator(props: DropdownMenuSeparatorProps) {
const [local, rest] = splitProps(props, ["class", "classList"])
return (
<Kobalte.Separator
{...rest}
data-slot="dropdown-menu-separator"
classList={{
...(local.classList ?? {}),
[local.class ?? ""]: !!local.class,
}}
/>
)
}
function DropdownMenuGroup(props: ParentProps<DropdownMenuGroupProps>) {
const [local, rest] = splitProps(props, ["class", "classList", "children"])
return (
<Kobalte.Group
{...rest}
data-slot="dropdown-menu-group"
classList={{
...(local.classList ?? {}),
[local.class ?? ""]: !!local.class,
}}
>
{local.children}
</Kobalte.Group>
)
}
function DropdownMenuGroupLabel(props: ParentProps<DropdownMenuGroupLabelProps>) {
const [local, rest] = splitProps(props, ["class", "classList", "children"])
return (
<Kobalte.GroupLabel
{...rest}
data-slot="dropdown-menu-group-label"
classList={{
...(local.classList ?? {}),
[local.class ?? ""]: !!local.class,
}}
>
{local.children}
</Kobalte.GroupLabel>
)
}
function DropdownMenuItem(props: ParentProps<DropdownMenuItemProps>) {
const [local, rest] = splitProps(props, ["class", "classList", "children"])
return (
<Kobalte.Item
{...rest}
data-slot="dropdown-menu-item"
classList={{
...(local.classList ?? {}),
[local.class ?? ""]: !!local.class,
}}
>
{local.children}
</Kobalte.Item>
)
}
function DropdownMenuItemLabel(props: ParentProps<DropdownMenuItemLabelProps>) {
const [local, rest] = splitProps(props, ["class", "classList", "children"])
return (
<Kobalte.ItemLabel
{...rest}
data-slot="dropdown-menu-item-label"
classList={{
...(local.classList ?? {}),
[local.class ?? ""]: !!local.class,
}}
>
{local.children}
</Kobalte.ItemLabel>
)
}
function DropdownMenuItemDescription(props: ParentProps<DropdownMenuItemDescriptionProps>) {
const [local, rest] = splitProps(props, ["class", "classList", "children"])
return (
<Kobalte.ItemDescription
{...rest}
data-slot="dropdown-menu-item-description"
classList={{
...(local.classList ?? {}),
[local.class ?? ""]: !!local.class,
}}
>
{local.children}
</Kobalte.ItemDescription>
)
}
function DropdownMenuItemIndicator(props: ParentProps<DropdownMenuItemIndicatorProps>) {
const [local, rest] = splitProps(props, ["class", "classList", "children"])
return (
<Kobalte.ItemIndicator
{...rest}
data-slot="dropdown-menu-item-indicator"
classList={{
...(local.classList ?? {}),
[local.class ?? ""]: !!local.class,
}}
>
{local.children}
</Kobalte.ItemIndicator>
)
}
function DropdownMenuRadioGroup(props: ParentProps<DropdownMenuRadioGroupProps>) {
const [local, rest] = splitProps(props, ["class", "classList", "children"])
return (
<Kobalte.RadioGroup
{...rest}
data-slot="dropdown-menu-radio-group"
classList={{
...(local.classList ?? {}),
[local.class ?? ""]: !!local.class,
}}
>
{local.children}
</Kobalte.RadioGroup>
)
}
function DropdownMenuRadioItem(props: ParentProps<DropdownMenuRadioItemProps>) {
const [local, rest] = splitProps(props, ["class", "classList", "children"])
return (
<Kobalte.RadioItem
{...rest}
data-slot="dropdown-menu-radio-item"
classList={{
...(local.classList ?? {}),
[local.class ?? ""]: !!local.class,
}}
>
{local.children}
</Kobalte.RadioItem>
)
}
function DropdownMenuCheckboxItem(props: ParentProps<DropdownMenuCheckboxItemProps>) {
const [local, rest] = splitProps(props, ["class", "classList", "children"])
return (
<Kobalte.CheckboxItem
{...rest}
data-slot="dropdown-menu-checkbox-item"
classList={{
...(local.classList ?? {}),
[local.class ?? ""]: !!local.class,
}}
>
{local.children}
</Kobalte.CheckboxItem>
)
}
function DropdownMenuSub(props: DropdownMenuSubProps) {
return <Kobalte.Sub {...props} />
}
function DropdownMenuSubTrigger(props: ParentProps<DropdownMenuSubTriggerProps>) {
const [local, rest] = splitProps(props, ["class", "classList", "children"])
return (
<Kobalte.SubTrigger
{...rest}
data-slot="dropdown-menu-sub-trigger"
classList={{
...(local.classList ?? {}),
[local.class ?? ""]: !!local.class,
}}
>
{local.children}
</Kobalte.SubTrigger>
)
}
function DropdownMenuSubContent(props: ParentProps<DropdownMenuSubContentProps>) {
const [local, rest] = splitProps(props, ["class", "classList", "children"])
return (
<Kobalte.SubContent
{...rest}
data-component="dropdown-menu-sub-content"
classList={{
...(local.classList ?? {}),
[local.class ?? ""]: !!local.class,
}}
>
{local.children}
</Kobalte.SubContent>
)
}
export const DropdownMenu = Object.assign(DropdownMenuRoot, {
Trigger: DropdownMenuTrigger,
Icon: DropdownMenuIcon,
Portal: DropdownMenuPortal,
Content: DropdownMenuContent,
Arrow: DropdownMenuArrow,
Separator: DropdownMenuSeparator,
Group: DropdownMenuGroup,
GroupLabel: DropdownMenuGroupLabel,
Item: DropdownMenuItem,
ItemLabel: DropdownMenuItemLabel,
ItemDescription: DropdownMenuItemDescription,
ItemIndicator: DropdownMenuItemIndicator,
RadioGroup: DropdownMenuRadioGroup,
RadioItem: DropdownMenuRadioItem,
CheckboxItem: DropdownMenuCheckboxItem,
Sub: DropdownMenuSub,
SubTrigger: DropdownMenuSubTrigger,
SubContent: DropdownMenuSubContent,
})

View File

@@ -133,6 +133,7 @@ const newIcons = {
"magnifying-glass": `<path d="M15.8332 15.8337L13.0819 13.0824M14.6143 9.39088C14.6143 12.2759 12.2755 14.6148 9.39039 14.6148C6.50532 14.6148 4.1665 12.2759 4.1665 9.39088C4.1665 6.5058 6.50532 4.16699 9.39039 4.16699C12.2755 4.16699 14.6143 6.5058 14.6143 9.39088Z" stroke="currentColor" stroke-linecap="square"/>`,
"plus-small": `<path d="M9.99984 5.41699V10.0003M9.99984 10.0003V14.5837M9.99984 10.0003H5.4165M9.99984 10.0003H14.5832" stroke="currentColor" stroke-linecap="square"/>`,
"chevron-down": `<path d="M6.6665 8.33325L9.99984 11.6666L13.3332 8.33325" stroke="currentColor" stroke-linecap="square"/>`,
"chevron-right": `<path d="M8.33301 13.3327L11.6663 9.99935L8.33301 6.66602" stroke="currentColor" stroke-linecap="square"/>`,
"arrow-up": `<path fill-rule="evenodd" clip-rule="evenodd" d="M9.99991 2.24121L16.0921 8.33343L15.2083 9.21731L10.6249 4.63397V17.5001H9.37492V4.63398L4.7916 9.21731L3.90771 8.33343L9.99991 2.24121Z" fill="currentColor"/>`,
"check-small": `<path d="M6.5 11.4412L8.97059 13.5L13.5 6.5" stroke="currentColor" stroke-linecap="square"/>`,
"edit-small-2": `<path d="M17.0834 17.0833V17.5833H17.5834V17.0833H17.0834ZM2.91675 17.0833H2.41675V17.5833H2.91675V17.0833ZM2.91675 2.91659V2.41659H2.41675V2.91659H2.91675ZM9.58341 3.41659H10.0834V2.41659H9.58341V2.91659V3.41659ZM17.5834 10.4166V9.91659H16.5834V10.4166H17.0834H17.5834ZM10.4167 7.08325L10.0632 6.7297L9.91675 6.87615V7.08325H10.4167ZM10.4167 9.58325H9.91675V10.0833H10.4167V9.58325ZM12.9167 9.58325V10.0833H13.1239L13.2703 9.93681L12.9167 9.58325ZM15.4167 2.08325L15.7703 1.7297L15.4167 1.37615L15.0632 1.7297L15.4167 2.08325ZM17.9167 4.58325L18.2703 4.93681L18.6239 4.58325L18.2703 4.2297L17.9167 4.58325ZM17.0834 17.0833V16.5833H2.91675V17.0833V17.5833H17.0834V17.0833ZM2.91675 17.0833H3.41675V2.91659H2.91675H2.41675V17.0833H2.91675ZM2.91675 2.91659V3.41659H9.58341V2.91659V2.41659H2.91675V2.91659ZM17.0834 10.4166H16.5834V17.0833H17.0834H17.5834V10.4166H17.0834ZM10.4167 7.08325H9.91675V9.58325H10.4167H10.9167V7.08325H10.4167ZM10.4167 9.58325V10.0833H12.9167V9.58325V9.08325H10.4167V9.58325ZM10.4167 7.08325L10.7703 7.43681L15.7703 2.43681L15.4167 2.08325L15.0632 1.7297L10.0632 6.7297L10.4167 7.08325ZM15.4167 2.08325L15.0632 2.43681L17.5632 4.93681L17.9167 4.58325L18.2703 4.2297L15.7703 1.7297L15.4167 2.08325ZM17.9167 4.58325L17.5632 4.2297L12.5632 9.2297L12.9167 9.58325L13.2703 9.93681L18.2703 4.93681L17.9167 4.58325Z" fill="currentColor"/>`,
@@ -170,6 +171,7 @@ const newIcons = {
"layout-bottom": `<path d="M18.125 18.125L1.875 18.125L1.875 1.875L18.125 1.875L18.125 18.125ZM3.125 12.8308L3.125 16.875L16.875 16.875L16.875 12.8308L3.125 12.8308ZM3.125 3.125L3.125 11.5808L16.875 11.5808L16.875 3.125L3.125 3.125Z" fill="currentColor"/>`,
"layout-bottom-partial": `<path d="M2.5 17.5L2.5 12.2059L17.5 12.2059L17.5 17.5L2.5 17.5Z" fill="currentColor" fill-opacity="40%" /><path d="M2.5 17.5L2.5 2.5M2.5 17.5L17.5 17.5M2.5 17.5L2.5 12.2059M2.5 2.5L17.5 2.5M2.5 2.5L2.5 12.2059M17.5 2.5L17.5 17.5M17.5 2.5L17.5 12.2059M17.5 17.5L17.5 12.2059M17.5 12.2059L2.5 12.2059" stroke="currentColor" stroke-linecap="square"/>`,
"layout-bottom-full": `<path d="M2.5 17.5L2.5 12.2059L17.5 12.2059L17.5 17.5L2.5 17.5Z" fill="currentColor"/><path d="M2.5 17.5L2.5 2.5M2.5 17.5L17.5 17.5M2.5 17.5L2.5 12.2059M2.5 2.5L17.5 2.5M2.5 2.5L2.5 12.2059M17.5 2.5L17.5 17.5M17.5 2.5L17.5 12.2059M17.5 17.5L17.5 12.2059M17.5 12.2059L2.5 12.2059" stroke="currentColor" stroke-linecap="square"/>`,
"dot-grid": `<path d="M2.08398 9.16602H3.75065V10.8327H2.08398V9.16602Z" fill="currentColor"/><path d="M10.834 9.16602H9.16732V10.8327H10.834V9.16602Z" fill="currentColor"/><path d="M16.2507 9.16602H17.9173V10.8327H16.2507V9.16602Z" fill="currentColor"/><path d="M2.08398 9.16602H3.75065V10.8327H2.08398V9.16602Z" stroke="currentColor"/><path d="M10.834 9.16602H9.16732V10.8327H10.834V9.16602Z" stroke="currentColor"/><path d="M16.2507 9.16602H17.9173V10.8327H16.2507V9.16602Z" stroke="currentColor"/>`,
}
export interface IconProps extends ComponentProps<"svg"> {

View File

@@ -1,4 +1,4 @@
import { UserMessage } from "@opencode-ai/sdk"
import { UserMessage } from "@opencode-ai/sdk/v2"
import { ComponentProps, createMemo, For, Match, Show, splitProps, Switch } from "solid-js"
import { DiffChanges } from "./diff-changes"
import { Spinner } from "./spinner"

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