Compare commits

..

89 Commits

Author SHA1 Message Date
opencode 2517b22552 release: v0.15.5 2025-10-16 18:06:59 +00:00
Aiden Cline 64617c113a ignore: tweak permissions 2025-10-16 12:48:39 -05:00
Aiden Cline 860c6338fc fix: github action (#3223) 2025-10-16 12:33:33 -05:00
Aiden Cline 4a7551e87b ci: fix changelog generation 2025-10-16 11:29:58 -05:00
Moisès Macià 285cc4b9fd docs: fix misspelled word (#3211) 2025-10-16 10:04:11 -05:00
Dax Raad d8a15e7bc9 try to avoid persisting empty thinking/text blocks 2025-10-16 10:54:10 -04:00
opencode 542b9fa342 release: v0.15.4 2025-10-16 14:53:32 +00:00
GitHub Action 9159afb54b ignore: update download stats 2025-10-16 2025-10-16 12:04:48 +00:00
seridescent 536934548a fix: use ai-sdk openai chat language model instead of completion language model (#3204) 2025-10-16 00:59:49 -05:00
Aiden Cline 1c59530115 Revert "fix: Text content blocks must contain non-whitespace text" (#3200) 2025-10-15 20:02:17 -05:00
Dax Raad ab8471a7ff core: filter out alpha status models from provider list 2025-10-15 20:12:37 -04:00
Dax Raad 4c674b075b ci: stuff 2025-10-15 19:59:46 -04:00
Dax Raad ba8a4c5e9f snapshot publish everything 2025-10-15 19:53:14 -04:00
Dax Raad 790fe72f39 sync 2025-10-15 19:48:57 -04:00
Aiden Cline 2d2d4641cb ignore: update readme 2025-10-15 16:06:11 -05:00
Aiden Cline d3caa55c10 Revert "fix: spawns hanging (#3192)"
This reverts commit 278ffb9a4e.
2025-10-15 15:20:14 -05:00
Fabian Kukuck ca534a36e5 feat: make compact feature use streaming API (#3079)
Co-authored-by: fku <fabian.kukuck@ipt.ch>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-10-15 13:44:16 -05:00
Aiden Cline 278ffb9a4e fix: spawns hanging (#3192) 2025-10-15 13:01:54 -05:00
Aiden Cline b2ff4be4c6 fix: Text content blocks must contain non-whitespace text (#3194) 2025-10-15 13:00:26 -05:00
Frank 2267ce2511 zen: support haiku 4.5 2025-10-15 13:53:00 -04:00
Matt Gillard e29d1d339c updated bedrock provider for the new Australian sonnet 4.5 cross region inference (#3050)
Co-authored-by: Matt Gillard <matt-github@gillard.biz>
2025-10-15 11:09:22 -05:00
Haris Gušić 92bc78a2d3 Improve http error codes (#3186) 2025-10-15 10:53:09 -05:00
GitHub Action 1ba5535460 ignore: update download stats 2025-10-15 2025-10-15 12:04:51 +00:00
Frank 7fa9a73bf0 wip: zen 2025-10-15 02:55:01 -04:00
Aiden Cline b3fcc9a81d tweak: consolidate session lock logic (#3185) 2025-10-15 01:12:51 -05:00
Frank e8751d976e wip: zen 2025-10-15 01:07:57 -04:00
Frank 43c9702aa7 wip: zen 2025-10-15 00:02:38 -04:00
Frank ae609be710 wip: zen 2025-10-14 23:45:06 -04:00
Frank 86ee36f562 wip: zen 2025-10-14 23:38:21 -04:00
Frank 0657f09139 wip: zen 2025-10-14 23:04:41 -04:00
Frank 182949dee4 wip: zen 2025-10-14 23:03:13 -04:00
Dax Raad 83655a3b09 ci: run typecheck before tests to catch type errors early 2025-10-14 18:36:03 -04:00
Dax Raad 62e5f4b154 try tsgo 2025-10-14 18:30:32 -04:00
Jay V ea926f0e1a ignore: prompt 2025-10-14 17:45:10 -04:00
Jay V 6191232d5f web: colocate copy button styles with components that use them 2025-10-14 17:41:17 -04:00
Frank 95f4ce86d6 ci: fix 2025-10-14 17:22:29 -04:00
Frank 5999aefde3 wip: zen 2025-10-14 17:18:39 -04:00
Frank babe3a0f40 wip: zen 2025-10-14 17:13:21 -04:00
Frank 29b95dee53 wip: zen 2025-10-14 17:06:49 -04:00
Frank ef9a1e911e wip: zen 2025-10-14 17:06:49 -04:00
Jay V 7eddaa806d docs: improve MCP server configuration guidance with examples and caveats 2025-10-14 16:43:59 -04:00
Dax Raad d07e79e6ad ci: channels 2025-10-14 15:09:18 -04:00
Dax Raad f17a7cde8d sync 2025-10-14 14:57:34 -04:00
GitHub Action 6d446c2a03 chore: format code 2025-10-14 18:56:54 +00:00
Dax Raad 61f6091de1 ci: test 2025-10-14 14:56:21 -04:00
Dax Raad 289783f627 ci: version stuff 2025-10-14 14:52:05 -04:00
Dax Raad 4c464cf4c0 ci: fix 2025-10-14 18:44:22 +00:00
opencode 83be5b0171 release: v0.15.3 2025-10-14 18:44:21 +00:00
Dax Raad 0c022ef39d ci: stuff 2025-10-14 14:35:04 -04:00
Aiden Cline 717b544633 fix: false positive package manager detection in upgrade (#3181) 2025-10-14 13:18:40 -05:00
Frank c1a420717a ci: fix 2025-10-14 13:58:54 -04:00
Frank 42c2ffd842 wip: zen 2025-10-14 13:52:30 -04:00
GitHub Action 5192c51843 chore: format code 2025-10-14 17:08:18 +00:00
Adam 96d7ccea48 wip: css/ui work 2025-10-14 12:07:45 -05:00
Adam 49e859cfd6 wip: css/ui work 2025-10-14 12:06:18 -05:00
Adam 6c57a69af4 wip: desktop work 2025-10-14 12:06:17 -05:00
Netanel Draiman 4d019430e2 feat(cli): add session option to attach command (#3167) 2025-10-14 11:04:32 -05:00
Adam 37e6c8342f wip: css and ui packages 2025-10-14 07:16:24 -05:00
Adam c04e892991 wip: desktop work 2025-10-14 07:15:08 -05:00
Adam bb82d43094 wip: desktop work 2025-10-14 07:15:08 -05:00
GitHub Action 2893b6e3a5 ignore: update download stats 2025-10-14 2025-10-14 12:04:50 +00:00
Dax 54c3361be7 feat: use realtime events for live tool call updates in Slack (#3163) 2025-10-14 03:13:48 -04:00
Dax Raad c50cf21f18 fix: update tsconfig for Slack package 2025-10-14 02:55:21 -04:00
Dax Raad cb73e2d9e1 fix: export trimDiff function from edit tool 2025-10-14 02:55:02 -04:00
Dax Raad 48057c2c21 fix: resolve TypeScript errors in SDK and Slack package 2025-10-14 02:54:37 -04:00
Dax Raad 1923ddab6e feat: add Slack integration package with Bolt framework 2025-10-14 02:53:55 -04:00
Dax Raad b8249cde4b core: improve dependency management and error handling for more reliable builds 2025-10-14 01:33:25 -04:00
Dax Raad 19b3f3d7ce core: standardize dependency versions for better reliability
Ensures consistent versions across packages by using workspace catalog for
tailwindcss and tsconfig dependencies, reducing potential conflicts and
installation issues.
2025-10-14 01:27:17 -04:00
Dax Raad e5e05d390d core: reduce dependency conflicts by standardizing package versions through catalog
This eliminates duplicate package versions that were causing build issues and
inconsistent behavior across the monorepo. Dependencies now resolve to single
versions through the workspace catalog, making installs faster and more reliable.
2025-10-14 01:23:54 -04:00
opencode 38ad6707cf release: v0.15.2 2025-10-14 04:56:00 +00:00
Alberto Fanton 7ef246f98f fix: disable GPG signing in snapshot tests (#3102) 2025-10-13 23:40:41 -05:00
Aiden Cline b91582d68a fix: config dir overrides (#3160) 2025-10-13 23:25:53 -05:00
Aiden Cline 682d30bd12 fix: custom model (#3156) 2025-10-13 19:58:19 -05:00
pancake 4d68ee5d2c fix: clang formatter name (#3042)
Co-authored-by: pancake <pancake@nopcode.org>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-10-13 19:55:18 -05:00
Haris Gušić dbe9fd00b7 fix: make shell more robust (#3051) 2025-10-13 17:37:35 -05:00
maple cd13a8524e docs: typo in custom-tools.mdx (#3152) 2025-10-13 17:19:20 -05:00
Aiden Cline 59765e0157 fix: typecheck (#3149) 2025-10-13 14:51:12 -05:00
AB d0519be0d0 fix: add useCompletionUrls option to fix certain azure setups (#2528)
Co-authored-by: andreas.blass <andreas.blass@outlook.com>
Co-authored-by: GitHub Action <action@github.com>
2025-10-13 14:16:21 -05:00
Tommy D. Rossi 066e4f064d tweak: include stack trace in server error responses (#3134) 2025-10-13 14:10:35 -05:00
opencode f81c469f17 release: v0.15.1 2025-10-13 18:14:52 +00:00
Dax Raad a398013ecb fix: disable workspace symbol lookup to prevent LSP performance issues 2025-10-13 14:05:54 -04:00
Aiden Cline 53d9717d90 fix: pass options to compact (#3136) 2025-10-13 10:42:39 -05:00
Aiden Cline 5885b691b9 docs: update recommended models list (#3121) 2025-10-12 21:35:31 -05:00
Aiden Cline fd70b9b057 fix: adjust list tool prompt to handle cwd better (#3115) 2025-10-12 16:48:03 -05:00
Ravshan Samandarov de13ccb757 docs: Update README.md (#3100) 2025-10-12 16:23:25 -04:00
Jay 7e1abb7bbf docs: Fix formatting of num_ctx in providers.mdx 2025-10-12 16:01:32 -04:00
Aiden Cline 83afcb9c42 docs: ollama num_ctx (#3111) 2025-10-12 10:40:51 -05:00
GitHub Action afb406c5ff ignore: update download stats 2025-10-12 2025-10-12 12:04:09 +00:00
OpeOginni 36cf9b9922 fix: add timeout to fetch models.dev refresh request (#3059) 2025-10-12 00:20:22 -05:00
130 changed files with 7108 additions and 1447 deletions
+4 -2
View File
@@ -8,7 +8,9 @@ jobs:
opencode:
if: |
contains(github.event.comment.body, ' /oc') ||
contains(github.event.comment.body, ' /opencode')
startsWith(github.event.comment.body, '/oc') ||
contains(github.event.comment.body, ' /opencode') ||
startsWith(github.event.comment.body, '/opencode')
runs-on: ubuntu-latest
permissions:
id-token: write
@@ -24,4 +26,4 @@ jobs:
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
with:
model: opencode/sonic
model: opencode/kimi-k2
+1
View File
@@ -58,6 +58,7 @@ jobs:
./script/publish.ts
env:
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_CHANNEL: latest
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
+1 -3
View File
@@ -28,9 +28,7 @@ jobs:
- name: Publish
run: |
./packages/opencode/script/publish.ts
./script/publish.ts
env:
OPENCODE_SNAPSHOT: true
OPENCODE_TAG: ${{ github.ref_name }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
+1
View File
@@ -24,6 +24,7 @@ jobs:
run: |
git config --global user.email "bot@opencode.ai"
git config --global user.name "opencode"
bun turbo typecheck
bun turbo test
env:
CI: true
+1 -1
View File
@@ -13,7 +13,7 @@ avoid repeating the title of the page, should be 5-10 words long
Chunks of text should not be more than 2 sentences long
Each section is spearated by a divider of 3 dashes
Each section is separated by a divider of 3 dashes
The section titles are short with only the first letter of the word capitalized
+8
View File
@@ -1,3 +1,7 @@
---
description: Git commit and push
---
commit and push
make sure it includes a prefix like
@@ -8,5 +12,9 @@ ci:
ignore:
wip:
For anything in the packages/web use the docs: prefix.
For anything in the packages/app use the ignore: prefix.
prefer to explain WHY something was done from an end user perspective instead of
WHAT was done.
+2 -1
View File
@@ -76,7 +76,7 @@ Take a look at the git history to see what kind of PRs we end up merging.
To run OpenCode locally you need.
- Bun
- Bun 1.3 or higher
- Golang 1.24.x
And run.
@@ -98,6 +98,7 @@ It's very similar to Claude Code in terms of capability. Here are the key differ
- 100% open source
- Not coupled to any provider. Although Anthropic is recommended, OpenCode can be used with OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider-agnostic is important.
- Out of the box LSP support
- A focus on TUI. OpenCode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
- A client/server architecture. This for example can allow OpenCode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients.
+110 -106
View File
@@ -1,108 +1,112 @@
# 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) |
| 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) |
+669 -204
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -14,6 +14,6 @@
"@actions/github": "6.0.1",
"@octokit/graphql": "9.0.1",
"@octokit/rest": "22.0.0",
"@opencode-ai/sdk": "0.5.4"
"@opencode-ai/sdk": "workspace:*"
}
}
+17 -5
View File
@@ -13,7 +13,8 @@
"packages": [
"packages/*",
"packages/console/*",
"packages/sdk/js"
"packages/sdk/js",
"packages/slack"
],
"catalog": {
"@types/bun": "1.3.0",
@@ -21,25 +22,35 @@
"@kobalte/core": "0.13.11",
"@types/node": "22.13.9",
"@tsconfig/node22": "22.0.2",
"@tsconfig/bun": "1.0.9",
"@cloudflare/workers-types": "4.20251008.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"diff": "8.0.2",
"ai": "5.0.8",
"hono": "4.7.10",
"fuzzysort": "3.1.0",
"luxon": "3.6.1",
"typescript": "5.8.2",
"@typescript/native-preview": "7.0.0-dev.20251014.1",
"zod": "4.1.8",
"remeda": "2.26.0",
"solid-js": "1.9.9"
"solid-js": "1.9.9",
"tailwindcss": "4.1.11",
"@tailwindcss/vite": "4.1.11",
"vite": "7.1.4",
"vite-plugin-solid": "2.11.8"
}
},
"devDependencies": {
"@tsconfig/bun": "1.0.9",
"@tsconfig/bun": "catalog:",
"husky": "9.1.7",
"prettier": "3.6.2",
"sst": "3.17.19",
"turbo": "2.5.6"
},
"dependencies": {
"@opencode-ai/sdk": "workspace:*"
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/script": "workspace:*"
},
"repository": {
"type": "git",
@@ -62,6 +73,7 @@
"@solidjs/start@1.1.7": "patches/@solidjs%2Fstart@1.1.7.patch"
},
"overrides": {
"@types/bun": "catalog:"
"@types/bun": "catalog:",
"@types/node": "catalog:"
}
}
+4 -3
View File
@@ -2,12 +2,12 @@
"name": "@opencode-ai/console-app",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"typecheck": "tsgo --noEmit",
"dev": "vinxi dev --host 0.0.0.0",
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
"build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
"start": "vinxi start",
"version": "0.15.0"
"version": "0.15.5"
},
"dependencies": {
"@ibm/plex": "6.4.1",
@@ -23,7 +23,8 @@
"zod": "catalog:"
},
"devDependencies": {
"typescript": "catalog:"
"typescript": "catalog:",
"@typescript/native-preview": "catalog:"
},
"engines": {
"node": ">=22"
@@ -4,9 +4,11 @@ import { A, createAsync } from "@solidjs/router"
import { createMemo, Match, Show, Switch } from "solid-js"
import { createStore } from "solid-js/store"
import { github } from "~/lib/github"
import { queryIsLoggedIn } from "~/routes/workspace/common"
export function Header(props: { zen?: boolean }) {
const githubData = createAsync(() => github())
const isLoggedIn = createAsync(() => queryIsLoggedIn())
const starCount = createMemo(() =>
githubData()?.stars
? new Intl.NumberFormat("en-US", {
@@ -39,7 +41,7 @@ export function Header(props: { zen?: boolean }) {
<li>
<Switch>
<Match when={props.zen}>
<a href="/auth">Login</a>
<a href="/auth">{isLoggedIn() ? "Workspace" : "Login"}</a>
</Match>
<Match when={!props.zen}>
<A href="/zen">Zen</A>
@@ -110,7 +112,7 @@ export function Header(props: { zen?: boolean }) {
<li>
<Switch>
<Match when={props.zen}>
<a href="/auth">Login</a>
<a href="/auth">{isLoggedIn() ? "Workspace" : "Login"}</a>
</Match>
<Match when={!props.zen}>
<A href="/zen">Zen</A>
@@ -118,3 +118,86 @@ export function IconWorkspaceLogo(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
</svg>
)
}
export function IconOpenAI(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" fill="none">
<path
d="M8.43799 8.06943V6.09387C8.43799 5.92749 8.50347 5.80267 8.65601 5.71959L12.8206 3.43211C13.3875 3.1202 14.0635 2.9747 14.7611 2.9747C17.3775 2.9747 19.0347 4.9087 19.0347 6.96734C19.0347 7.11288 19.0347 7.27926 19.0128 7.44564L14.6956 5.03335C14.434 4.88785 14.1723 4.88785 13.9107 5.03335L8.43799 8.06943ZM18.1624 15.7637V11.0431C18.1624 10.7519 18.0315 10.544 17.7699 10.3984L12.2972 7.36234L14.0851 6.3849C14.2377 6.30182 14.3686 6.30182 14.5212 6.3849L18.6858 8.67238C19.8851 9.3379 20.6917 10.7519 20.6917 12.1243C20.6917 13.7047 19.7106 15.1604 18.1624 15.7636V15.7637ZM7.15158 11.6047L5.36369 10.6066C5.21114 10.5235 5.14566 10.3986 5.14566 10.2323V5.65735C5.14566 3.43233 6.93355 1.7478 9.35381 1.7478C10.2697 1.7478 11.1199 2.039 11.8396 2.55886L7.54424 4.92959C7.28268 5.07508 7.15181 5.28303 7.15181 5.57427V11.6049L7.15158 11.6047ZM11 13.7258L8.43799 12.3533V9.44209L11 8.06965L13.5618 9.44209V12.3533L11 13.7258ZM12.6461 20.0476C11.7303 20.0476 10.8801 19.7564 10.1604 19.2366L14.4557 16.8658C14.7173 16.7203 14.8482 16.5124 14.8482 16.2211V10.1905L16.658 11.1886C16.8105 11.2717 16.876 11.3965 16.876 11.563V16.1379C16.876 18.3629 15.0662 20.0474 12.6461 20.0474V20.0476ZM7.47863 15.4103L3.314 13.1229C2.11471 12.4573 1.30808 11.0433 1.30808 9.67088C1.30808 8.06965 2.31106 6.6348 3.85903 6.03168V10.773C3.85903 11.0642 3.98995 11.2721 4.25151 11.4177L9.70253 14.4328L7.91464 15.4103C7.76209 15.4934 7.63117 15.4934 7.47863 15.4103ZM7.23892 18.8207C4.77508 18.8207 2.96533 17.0531 2.96533 14.8696C2.96533 14.7032 2.98719 14.5368 3.00886 14.3704L7.30418 16.7412C7.56574 16.8867 7.82752 16.8867 8.08909 16.7412L13.5618 13.726V15.7015C13.5618 15.8679 13.4964 15.9927 13.3438 16.0758L9.17918 18.3633C8.61225 18.6752 7.93631 18.8207 7.23869 18.8207H7.23892ZM12.6461 21.2952C15.2844 21.2952 17.4865 19.5069 17.9882 17.1362C20.4301 16.5331 22 14.3495 22 12.1245C22 10.6688 21.346 9.25482 20.1685 8.23581C20.2775 7.79908 20.343 7.36234 20.343 6.92582C20.343 3.95215 17.8137 1.72691 14.892 1.72691C14.3034 1.72691 13.7365 1.80999 13.1695 1.99726C12.1882 1.08223 10.8364 0.5 9.35381 0.5C6.71557 0.5 4.51352 2.28829 4.01185 4.65902C1.56987 5.26214 0 7.44564 0 9.67067C0 11.1264 0.654039 12.5404 1.83147 13.5594C1.72246 13.9961 1.65702 14.4328 1.65702 14.8694C1.65702 17.8431 4.1863 20.0683 7.108 20.0683C7.69661 20.0683 8.26354 19.9852 8.83046 19.7979C9.81155 20.713 11.1634 21.2952 12.6461 21.2952Z"
fill="currentColor"
/>
</svg>
)
}
export function IconAnthropic(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M13.7891 3.93188L20.2223 20.068H23.7502L17.317 3.93188H13.7891Z" />
<path
fill="currentColor"
d="M6.32538 13.6827L8.52662 8.01201L10.7279 13.6827H6.32538ZM6.68225 3.93188L0.25 20.068H3.84652L5.16202 16.6794H11.8914L13.2067 20.068H16.8033L10.371 3.93188H6.68225Z"
/>
</svg>
)
}
export function IconXai(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path
d="M9.16861 16.0529L17.2018 9.85156C17.5957 9.54755 18.1586 9.66612 18.3463 10.1384C19.3339 12.6288 18.8926 15.6217 16.9276 17.6766C14.9626 19.7314 12.2285 20.1821 9.72948 19.1557L6.9995 20.4775C10.9151 23.2763 15.6699 22.5841 18.6411 19.4749C20.9979 17.0103 21.7278 13.6508 21.0453 10.6214L21.0515 10.6278C20.0617 6.17736 21.2948 4.39847 23.8207 0.760904C23.8804 0.674655 23.9402 0.588405 24 0.5L20.6762 3.97585V3.96506L9.16658 16.0551"
fill="currentColor"
/>
<path
d="M7.37742 16.7017C4.67579 14.0395 5.14158 9.91963 7.44676 7.54383C9.15135 5.78544 11.9442 5.06779 14.3821 6.12281L17.0005 4.87559C16.5288 4.52392 15.9242 4.14566 15.2305 3.87986C12.0948 2.54882 8.34069 3.21127 5.79171 5.8386C3.33985 8.36779 2.56881 12.2567 3.89286 15.5751C4.88192 18.0552 3.26056 19.8094 1.62731 21.5801C1.04853 22.2078 0.467774 22.8355 0 23.5L7.3754 16.7037"
fill="currentColor"
/>
</svg>
)
}
export function IconAlibaba(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" fill="none">
<path
fill-rule="evenodd"
clip-rule="evenodd"
fill="currentColor"
d="M11.6043 0.340162C11.9973 1.03016 12.3883 1.72215 12.7783 2.41514C12.7941 2.44286 12.8169 2.46589 12.8445 2.48187C12.8721 2.49786 12.9034 2.50624 12.9353 2.50614H18.4873C18.6612 2.50614 18.8092 2.61614 18.9332 2.83314L20.3872 5.40311C20.5772 5.74011 20.6272 5.88111 20.4112 6.24011C20.1512 6.6701 19.8982 7.1041 19.6512 7.54009L19.2842 8.19809C19.1782 8.39409 19.0612 8.47809 19.2442 8.71008L21.8962 13.347C22.0682 13.648 22.0072 13.841 21.8532 14.117C21.4162 14.902 20.9712 15.681 20.5182 16.457C20.3592 16.729 20.1662 16.832 19.8382 16.827C19.0612 16.811 18.2863 16.817 17.5113 16.843C17.4946 16.8439 17.4785 16.8489 17.4644 16.8576C17.4502 16.8664 17.4385 16.8785 17.4303 16.893C16.5361 18.4773 15.6344 20.0573 14.7253 21.633C14.5563 21.926 14.3453 21.996 14.0003 21.997C13.0033 22 11.9983 22.001 10.9833 21.999C10.8889 21.9987 10.7961 21.9735 10.7145 21.9259C10.6328 21.8783 10.5652 21.8101 10.5184 21.728L9.18337 19.405C9.1756 19.3898 9.16368 19.3771 9.14898 19.3684C9.13429 19.3598 9.11743 19.3554 9.10037 19.356H3.98244C3.69744 19.386 3.42944 19.355 3.17745 19.264L1.57447 16.494C1.52706 16.412 1.50193 16.319 1.50158 16.2243C1.50123 16.1296 1.52567 16.0364 1.57247 15.954L2.77945 13.834C2.79665 13.8041 2.80569 13.7701 2.80569 13.7355C2.80569 13.701 2.79665 13.667 2.77945 13.637C2.15073 12.5485 1.52573 11.4579 0.904476 10.3651L0.114486 8.97008C-0.0455115 8.66008 -0.0585113 8.47409 0.209485 8.00509C0.674479 7.1921 1.13647 6.38011 1.59647 5.56911C1.72847 5.33512 1.90046 5.23512 2.18046 5.23412C3.04344 5.23048 3.90644 5.23015 4.76943 5.23312C4.79123 5.23295 4.81259 5.22704 4.83138 5.21597C4.85016 5.20491 4.8657 5.1891 4.87643 5.17012L7.68239 0.275163C7.72491 0.200697 7.78631 0.138751 7.86039 0.0955646C7.93448 0.0523783 8.01863 0.0294762 8.10439 0.0291651C8.62838 0.0281651 9.15737 0.029165 9.68736 0.0231651L10.7044 0.000165317C11.0453 -0.00283466 11.4283 0.032165 11.6043 0.340162ZM8.17238 0.743158C8.16185 0.743152 8.15149 0.745921 8.14236 0.751187C8.13323 0.756454 8.12565 0.764031 8.12038 0.773158L5.25442 5.78811C5.24066 5.81174 5.22097 5.83137 5.19729 5.84505C5.17361 5.85873 5.14677 5.86599 5.11942 5.86611H2.25346C2.19746 5.86611 2.18346 5.89111 2.21246 5.94011L8.02239 16.096C8.04739 16.138 8.03539 16.158 7.98839 16.159L5.19342 16.174C5.15256 16.1727 5.11214 16.1828 5.07678 16.2033C5.04141 16.2238 5.01253 16.2539 4.99342 16.29L3.67344 18.6C3.62944 18.678 3.65244 18.718 3.74144 18.718L9.45737 18.726C9.50337 18.726 9.53737 18.746 9.56137 18.787L10.9643 21.241C11.0103 21.322 11.0563 21.323 11.1033 21.241L16.1093 12.481L16.8923 11.0991C16.897 11.0905 16.904 11.0834 16.9125 11.0785C16.9209 11.0735 16.9305 11.0709 16.9403 11.0709C16.9501 11.0709 16.9597 11.0735 16.9681 11.0785C16.9765 11.0834 16.9835 11.0905 16.9883 11.0991L18.4123 13.629C18.4229 13.648 18.4385 13.6637 18.4573 13.6746C18.4761 13.6855 18.4975 13.6912 18.5193 13.691L21.2822 13.671C21.2893 13.6711 21.2963 13.6693 21.3024 13.6658C21.3086 13.6623 21.3137 13.6572 21.3172 13.651C21.3206 13.6449 21.3224 13.638 21.3224 13.631C21.3224 13.624 21.3206 13.6172 21.3172 13.611L18.4173 8.52508C18.4068 8.50809 18.4013 8.48853 18.4013 8.46859C18.4013 8.44864 18.4068 8.42908 18.4173 8.41209L18.7102 7.90509L19.8302 5.92811C19.8542 5.88711 19.8422 5.86611 19.7952 5.86611H8.20038C8.14138 5.86611 8.12738 5.84011 8.15738 5.78911L9.59137 3.28413C9.60211 3.26706 9.60781 3.24731 9.60781 3.22714C9.60781 3.20697 9.60211 3.18721 9.59137 3.17014L8.22538 0.774158C8.22016 0.764697 8.21248 0.756822 8.20315 0.751365C8.19382 0.745909 8.18319 0.743073 8.17238 0.743158ZM14.4623 8.76308C14.5083 8.76308 14.5203 8.78308 14.4963 8.82308L13.6643 10.2881L11.0513 14.873C11.0464 14.8819 11.0392 14.8894 11.0304 14.8945C11.0216 14.8996 11.0115 14.9022 11.0013 14.902C10.9912 14.902 10.9813 14.8993 10.9725 14.8942C10.9637 14.8891 10.9564 14.8818 10.9513 14.873L7.49839 8.84108C7.47839 8.80708 7.48839 8.78908 7.52639 8.78708L7.74239 8.77508L14.4643 8.76308H14.4623Z"
/>
</svg>
)
}
export function IconMoonshotAI(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M20.052 6.0364C18.7527 4.28846 16.8758 2.94985 14.6066 2.34296C12.3374 1.73606 10.0411 1.95816 8.04092 2.82331L20.052 6.0364ZM3.75774 6.34071C4.64576 5.04902 5.81872 3.99836 7.16325 3.25459L16.0115 5.62191C15.4308 5.95635 14.8088 6.53268 14.3273 7.16626L21.6025 9.11263C21.809 9.79398 21.9435 10.5003 22 11.2213L3.75774 6.34071ZM21.6866 14.5876C21.584 14.9707 21.4603 15.3425 21.3172 15.7019L2.10543 10.5623C2.16147 10.1792 2.24079 9.7957 2.34339 9.41263C2.58479 8.51262 2.94172 7.67459 3.39435 6.91016L12.7957 9.42554C12.4194 9.96271 12.0766 10.5464 11.7749 11.1709L21.8517 13.8671C21.8056 14.1077 21.7504 14.3478 21.6862 14.5884L21.6866 14.5876ZM2.58134 15.3529C2.11535 14.05 1.91662 12.6408 2.03215 11.2088L10.8985 13.5808C10.8347 13.7823 10.7748 13.9863 10.7192 14.1933C10.6062 14.6147 10.514 15.0352 10.4424 15.4527L20.0166 18.0142C19.5709 18.6051 19.0631 19.1406 18.5057 19.6132L2.58134 15.3529ZM9.42338 21.6568C6.40111 20.8481 4.07415 18.7424 2.88266 16.1001L10.0976 18.0305C10.0859 18.7024 10.1278 19.3571 10.2196 19.9851L15.4619 21.3874C13.5906 22.0743 11.496 22.2112 9.42338 21.6568Z"
fill="currentColor"
/>
</svg>
)
}
export function IconZai(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12.105 2L9.927 4.953H.653L2.83 2h9.276zM23.254 19.048L21.078 22h-9.242l2.174-2.952h9.244zM24 2L9.264 22H0L14.736 2H24z"></path>
</svg>
)
}
export function IconStealth(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 18" fill="none">
<path
d="M24 15.5L18.1816 14.2871L18.1328 14.3115C16.9036 11.7879 15.6135 9.29301 14.2607 6.82812C14.0172 6.38435 13.771 5.94188 13.5234 5.5C13.6911 5.97998 13.8606 6.45942 14.0322 6.9375C14.9902 9.60529 16.012 12.2429 17.0947 14.8516L12 17.5L6.9043 14.8516C7.98712 12.2428 9.00977 9.6054 9.96777 6.9375C10.1394 6.45942 10.3089 5.97998 10.4766 5.5C10.229 5.94188 9.98281 6.38435 9.73926 6.82812C8.38629 9.29339 7.09557 11.7884 5.86621 14.3125L5.81738 14.2871L0 15.5L12 0.5L24 15.5Z"
fill="currentColor"
/>
</svg>
)
}
+1 -1
View File
@@ -43,7 +43,7 @@ export default function Home() {
<main data-page="opencode">
<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />
<Title>OpenCode | The AI coding agent built for the terminal</Title>
<Link rel="icon" type="image/svg+xml" href="/favicon-zen.svg" />
<Link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<Meta property="og:image" content="/social-share.png" />
<Meta name="twitter:image" content="/social-share.png" />
<div data-component="container">
@@ -1,4 +1,3 @@
import { Show } from "solid-js"
import { query, createAsync, RouteSectionProps, useParams, A } from "@solidjs/router"
import "./workspace.css"
import { IconWorkspaceLogo } from "../component/icon"
@@ -7,7 +6,7 @@ import { UserMenu } from "./user-menu"
import { withActor } from "~/context/auth.withActor"
import { User } from "@opencode-ai/console-core/user.js"
import { Actor } from "@opencode-ai/console-core/actor.js"
import { querySessionInfo } from "./workspace/common"
import { Link } from "@solidjs/meta"
const getUserEmail = query(async (workspaceID: string) => {
"use server"
@@ -21,17 +20,15 @@ const getUserEmail = query(async (workspaceID: string) => {
export default function WorkspaceLayout(props: RouteSectionProps) {
const params = useParams()
const userEmail = createAsync(() => getUserEmail(params.id))
const sessionInfo = createAsync(() => querySessionInfo(params.id))
return (
<main data-page="workspace">
<Link rel="icon" type="image/svg+xml" href="/favicon-zen.svg" />
<header data-component="workspace-header">
<div data-slot="header-brand">
<A href="/" data-component="site-title">
<IconWorkspaceLogo />
</A>
<Show when={sessionInfo()?.isBeta}>
<WorkspacePicker />
</Show>
<WorkspacePicker />
</div>
<div data-slot="header-actions">
<UserMenu email={userEmail()} />
@@ -96,7 +96,7 @@ export function PaymentSection() {
}}
data-slot="receipt-button"
>
view
View
</button>
</td>
</tr>
@@ -52,7 +52,7 @@ export default function () {
}
>
<span data-slot="balance">
Current balance: <b>${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()}</b>
Current balance <b>${balanceAmount() === "-0.00" ? "0.00" : balanceAmount()}</b>
</span>
</Show>
</span>
@@ -34,6 +34,12 @@
color: var(--color-text);
font-family: var(--font-mono);
font-weight: 500;
div {
display: flex;
align-items: center;
gap: 8px;
}
}
&[data-slot="training-data"] {
@@ -88,8 +94,8 @@
}
/* Checked state - track */
input:checked+span {
background-color: #21AD0E;
input:checked + span {
background-color: #21ad0e;
border-color: #148605;
/* Checked state - handle */
@@ -103,7 +109,7 @@
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2);
}
input:checked:hover+span {
input:checked:hover + span {
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.3);
}
@@ -112,16 +118,16 @@
cursor: not-allowed;
}
input:disabled+span {
input:disabled + span {
opacity: 0.5;
cursor: not-allowed;
}
input:disabled:checked+span {
input:disabled:checked + span {
opacity: 0.5;
}
input:disabled~span:hover {
input:disabled ~ span:hover {
box-shadow: none;
}
}
@@ -142,7 +148,6 @@
@media (max-width: 40rem) {
[data-slot="models-table-element"] {
th,
td {
padding: var(--space-2) var(--space-3);
@@ -152,8 +157,7 @@
th {
&:nth-child(2)
/* Training Data */
{
/* Training Data */ {
display: none;
}
}
@@ -161,10 +165,9 @@
td {
&:nth-child(2)
/* Training Data */
{
/* Training Data */ {
display: none;
}
}
}
}
}
@@ -5,13 +5,25 @@ import { withActor } from "~/context/auth.withActor"
import { ZenModel } from "@opencode-ai/console-core/model.js"
import styles from "./model-section.module.css"
import { querySessionInfo } from "../common"
import { IconAlibaba, IconAnthropic, IconMoonshotAI, IconOpenAI, IconStealth, IconXai, IconZai } from "~/component/icon"
const getModelLab = (modelId: string) => {
if (modelId.startsWith("claude")) return "Anthropic"
if (modelId.startsWith("gpt")) return "OpenAI"
if (modelId.startsWith("kimi")) return "Moonshot AI"
if (modelId.startsWith("glm")) return "Z.ai"
if (modelId.startsWith("qwen")) return "Alibaba"
if (modelId.startsWith("grok")) return "xAI"
return "Stealth"
}
const getModelsInfo = query(async (workspaceID: string) => {
"use server"
return withActor(async () => {
return {
all: Object.entries(ZenModel.list())
.filter(([id, _model]) => !["claude-3-5-haiku", "qwen3-max"].includes(id))
.filter(([id, _model]) => !["claude-3-5-haiku"].includes(id))
.filter(([id, _model]) => !id.startsWith("an-"))
.sort(([_idA, modelA], [_idB, modelB]) => modelA.name.localeCompare(modelB.name))
.map(([id, model]) => ({ id, name: model.name })),
disabled: await Model.listDisabled(),
@@ -42,13 +54,21 @@ export function ModelSection() {
const params = useParams()
const modelsInfo = createAsync(() => getModelsInfo(params.id))
const userInfo = createAsync(() => querySessionInfo(params.id))
const modelsWithLab = createMemo(() => {
const info = modelsInfo()
if (!info) return []
return info.all.map((model) => ({
...model,
lab: getModelLab(model.id),
}))
})
return (
<section class={styles.root}>
<div data-slot="section-title">
<h2>Models</h2>
<p>
Manage which models workspace members can access. Requests will fail if a member tries to use a disabled
model.{userInfo()?.isAdmin ? "" : " To use a disabled model, contact your workspaces admin."}
Manage which models workspace members can access. <a href="/docs/zen#pricing ">Learn more</a>.
</p>
</div>
<div data-slot="models-list">
@@ -58,16 +78,40 @@ export function ModelSection() {
<thead>
<tr>
<th>Model</th>
<th></th>
<th>Enabled</th>
</tr>
</thead>
<tbody>
<For each={modelsInfo()!.all}>
{({ id, name }) => {
<For each={modelsWithLab()}>
{({ id, name, lab }) => {
const isEnabled = createMemo(() => !modelsInfo()!.disabled.includes(id))
return (
<tr data-slot="model-row" data-disabled={!isEnabled()}>
<td data-slot="model-name">{name}</td>
<td data-slot="model-name">
<div>
{(() => {
switch (lab) {
case "OpenAI":
return <IconOpenAI width={16} height={16} />
case "Anthropic":
return <IconAnthropic width={16} height={16} />
case "Moonshot AI":
return <IconMoonshotAI width={16} height={16} />
case "Z.ai":
return <IconZai width={16} height={16} />
case "Alibaba":
return <IconAlibaba width={16} height={16} />
case "xAI":
return <IconXai width={16} height={16} />
default:
return <IconStealth width={16} height={16} />
}
})()}
<span>{name}</span>
</div>
</td>
<td data-slot="model-lab">{lab}</td>
<td data-slot="model-toggle">
<form action={updateModel} method="post">
<input type="hidden" name="model" value={id} />
@@ -30,6 +30,18 @@ export function formatDateUTC(date: Date) {
return date.toLocaleDateString("en-US", options)
}
export const queryIsLoggedIn = query(async () => {
"use server"
return withActor(() => {
try {
Actor.assert("account")
return true
} catch {
return false
}
})
}, "isLoggedIn.get")
export const querySessionInfo = query(async (workspaceID: string) => {
"use server"
return withActor(() => {
@@ -1,4 +1,5 @@
import "./index.css"
import { createAsync } from "@solidjs/router"
import { Title, Meta, Link } from "@solidjs/meta"
import { HttpHeader } from "@solidjs/start"
import zenLogoLight from "../../asset/zen-ornate-light.svg"
@@ -15,8 +16,10 @@ import { Faq } from "~/component/faq"
import { Legal } from "~/component/legal"
import { Footer } from "~/component/footer"
import { Header } from "~/component/header"
import { queryIsLoggedIn } from "~/routes/workspace/common"
export default function Home() {
const isLoggedIn = createAsync(() => queryIsLoggedIn())
return (
<main data-page="zen">
<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />
@@ -102,7 +105,7 @@ export default function Home() {
</div>
</div>
<a href="/auth">
<span>Get started with Zen </span>
<span>{isLoggedIn() ? "Go to workspace " : "Get started with Zen "}</span>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M6.5 12L17 12M13 16.5L17.5 12L13 7.5"
+6 -5
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "0.15.0",
"version": "0.15.5",
"private": true,
"type": "module",
"dependencies": {
@@ -27,15 +27,16 @@
"update-models": "script/update-models.ts",
"promote-models-to-dev": "script/promote-models.ts dev",
"promote-models-to-prod": "script/promote-models.ts production",
"typecheck": "tsc --noEmit"
"typecheck": "tsgo --noEmit"
},
"devDependencies": {
"@cloudflare/workers-types": "4.20251008.0",
"@cloudflare/workers-types": "catalog:",
"@tsconfig/node22": "22.0.2",
"@types/bun": "1.3.0",
"@types/node": "24.7.1",
"@types/node": "catalog:",
"drizzle-kit": "0.30.5",
"mysql2": "3.14.4",
"typescript": "catalog:"
"typescript": "catalog:",
"@typescript/native-preview": "catalog:"
}
}
+1 -1
View File
@@ -134,7 +134,7 @@ export namespace User {
const { InviteEmail } = await import("@opencode-ai/console-mail/InviteEmail.jsx")
await AWS.sendEmail({
to: email,
subject: `You've been invited to join the ${emailInfo.workspaceName} workspace on OpenCode Console`,
subject: `You've been invited to join the ${emailInfo.workspaceName} workspace on OpenCode`,
body: render(
// @ts-ignore
InviteEmail({
+5 -4
View File
@@ -1,18 +1,19 @@
{
"name": "@opencode-ai/console-function",
"version": "0.15.0",
"version": "0.15.5",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit"
"typecheck": "tsgo --noEmit"
},
"devDependencies": {
"@cloudflare/workers-types": "4.20251008.0",
"@cloudflare/workers-types": "catalog:",
"@tsconfig/node22": "22.0.2",
"@types/node": "catalog:",
"openai": "5.11.0",
"typescript": "catalog:"
"typescript": "catalog:",
"@typescript/native-preview": "catalog:"
},
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
+6
View File
@@ -1,3 +1,4 @@
import type { KVNamespace } from "@cloudflare/workers-types"
import { z } from "zod"
import { issuer } from "@openauthjs/openauth"
import type { Theme } from "@openauthjs/openauth/ui/theme"
@@ -94,6 +95,7 @@ export default {
// }),
},
storage: CloudflareStorage({
// @ts-ignore
namespace: env.AuthStorage,
}),
subjects,
@@ -118,6 +120,10 @@ export default {
if (!email) throw new Error("No email found")
if (Resource.App.stage !== "production" && !email.endsWith("@anoma.ly")) {
throw new Error("Invalid email")
}
let accountID = await Account.fromEmail(email).then((x) => x?.id)
if (!accountID) {
console.log("creating account for", email)
+7 -47
View File
@@ -1,26 +1,10 @@
// @ts-nocheck
import React from "react"
import { Font, Hr as JEHr, Text as JEText, type HrProps, type TextProps } from "@jsx-email/all"
import { DIVIDER_COLOR, SURFACE_DIVIDER_COLOR, textColor } from "./styles"
import { Font, Text as JEText, type TextProps } from "@jsx-email/all"
import { baseText } from "./styles"
export function Text(props: TextProps) {
return <JEText {...props} style={{ ...textColor, ...props.style }} />
}
export function Hr(props: HrProps) {
return <JEHr {...props} style={{ borderTop: `1px solid ${DIVIDER_COLOR}`, ...props.style }} />
}
export function SurfaceHr(props: HrProps) {
return (
<JEHr
{...props}
style={{
borderTop: `1px solid ${SURFACE_DIVIDER_COLOR}`,
...props.style,
}}
/>
)
return <JEText {...props} style={{ ...baseText, ...props.style }} />
}
export function Title({ children }: TitleProps) {
@@ -31,10 +15,6 @@ export function A({ children, ...props }: AProps) {
return React.createElement("a", props, children)
}
export function B({ children, ...props }: AProps) {
return React.createElement("b", props, children)
}
export function Span({ children, ...props }: SpanProps) {
return React.createElement("span", props, children)
}
@@ -47,45 +27,25 @@ export function Fonts({ assetsUrl }: { assetsUrl: string }) {
return (
<>
<Font
fontFamily="IBM Plex Mono"
fontFamily="JetBrains Mono"
fallbackFontFamily="monospace"
webFont={{
url: `${assetsUrl}/ibm-plex-mono-latin-400.woff2`,
url: `${assetsUrl}/JetBrainsMono-Regular.woff2`,
format: "woff2",
}}
fontWeight="400"
fontStyle="normal"
/>
<Font
fontFamily="IBM Plex Mono"
fontFamily="JetBrains Mono"
fallbackFontFamily="monospace"
webFont={{
url: `${assetsUrl}/ibm-plex-mono-latin-500.woff2`,
url: `${assetsUrl}/JetBrainsMono-Medium.woff2`,
format: "woff2",
}}
fontWeight="500"
fontStyle="normal"
/>
<Font
fontFamily="IBM Plex Mono"
fallbackFontFamily="monospace"
webFont={{
url: `${assetsUrl}/ibm-plex-mono-latin-600.woff2`,
format: "woff2",
}}
fontWeight="600"
fontStyle="normal"
/>
<Font
fontFamily="IBM Plex Mono"
fallbackFontFamily="monospace"
webFont={{
url: `${assetsUrl}/ibm-plex-mono-latin-700.woff2`,
format: "woff2",
}}
fontWeight="700"
fontStyle="normal"
/>
<Font
fontFamily="Rubik"
fallbackFontFamily={["Helvetica", "Arial", "sans-serif"]}
+70 -89
View File
@@ -1,110 +1,91 @@
export const unit = 16;
export const GREY_COLOR = [
"#1A1A2E", //0
"#2F2F41", //1
"#444454", //2
"#585867", //3
"#6D6D7A", //4
"#82828D", //5
"#9797A0", //6
"#ACACB3", //7
"#C1C1C6", //8
"#D5D5D9", //9
"#EAEAEC", //10
"#FFFFFF", //11
];
export const BLUE_COLOR = "#395C6B";
export const DANGER_COLOR = "#ED322C";
export const TEXT_COLOR = GREY_COLOR[0];
export const SECONDARY_COLOR = GREY_COLOR[5];
export const DIMMED_COLOR = GREY_COLOR[7];
export const DIVIDER_COLOR = GREY_COLOR[10];
export const BACKGROUND_COLOR = "#F0F0F1";
export const SURFACE_COLOR = DIVIDER_COLOR;
export const SURFACE_DIVIDER_COLOR = GREY_COLOR[9];
// @ts-nocheck
export const unit = 12
export const PRIMARY_COLOR = "#211E1E"
export const TEXT_COLOR = "#656363"
export const LINK_COLOR = "#007AFF"
export const LINK_BACKGROUND_COLOR = "#F9F8F8"
export const BACKGROUND_COLOR = "#F0F0F1"
export const SURFACE_DIVIDER_COLOR = "#D5D5D9"
export const body = {
background: BACKGROUND_COLOR,
};
}
export const container = {
minWidth: "600px",
};
export const medium = {
fontWeight: 500,
};
export const danger = {
color: DANGER_COLOR,
};
padding: "64px 0px",
}
export const frame = {
padding: `${unit * 1.5}px`,
padding: `${unit * 2}px`,
border: `1px solid ${SURFACE_DIVIDER_COLOR}`,
background: "#FFF",
borderRadius: "6px",
boxShadow: `0 1px 2px rgba(0,0,0,0.03),
0 2px 4px rgba(0,0,0,0.03),
0 2px 6px rgba(0,0,0,0.03)`,
};
}
export const textColor = {
export const baseText = {
fontFamily: "JetBrains Mono, monospace",
}
export const headingText = {
color: PRIMARY_COLOR,
fontSize: "16px",
fontStyle: "normal",
fontWeight: 500,
lineHeight: "normal",
}
export const contentText = {
color: TEXT_COLOR,
};
fontSize: "14px",
fontStyle: "normal",
fontWeight: 400,
lineHeight: "180%",
}
export const code = {
fontFamily: "IBM Plex Mono, monospace",
};
export const buttonText = {
color: "#FDFCFC",
fontSize: "16px",
fontWeight: 500,
margin: 0,
padding: 0,
display: "inline-flex",
alignItems: "center",
gap: "12px",
}
export const headingHr = {
margin: `${unit}px 0`,
};
export const buttonPrimary = {
...code,
padding: "12px 18px",
color: "#FFF",
export const linkText = {
color: LINK_COLOR,
fontSize: "14px",
fontStyle: "normal",
fontWeight: 400,
lineHeight: "150%",
textDecorationLine: "underline",
textDecorationStyle: "solid" as const,
textDecorationSkipInk: "auto" as const,
textDecorationThickness: "auto",
textUnderlineOffset: "auto",
textUnderlinePosition: "from-font",
borderRadius: "4px",
background: BLUE_COLOR,
fontSize: "12px",
fontWeight: 500,
};
background: LINK_BACKGROUND_COLOR,
padding: "8px 12px",
textAlign: "center" as const,
}
export const compactText = {
margin: "0 0 2px",
};
export const contentHighlightText = {
color: PRIMARY_COLOR,
}
export const breadcrumb = {
fontSize: "14px",
color: SECONDARY_COLOR,
};
export const breadcrumbColonSeparator = {
padding: " 0 4px",
color: DIMMED_COLOR,
};
export const breadcrumbSeparator = {
color: DIVIDER_COLOR,
};
export const heading = {
fontSize: "22px",
fontWeight: 500,
};
export const sectionLabel = {
...code,
...compactText,
letterSpacing: "0.5px",
fontSize: "13px",
fontWeight: 500,
color: DIMMED_COLOR,
};
export const footerLink = {
fontSize: "14px",
};
export const button = {
display: "inline-grid",
padding: "8px 12px 8px 20px",
justifyContent: "center",
alignItems: "center",
gap: "8px",
flexShrink: "0",
borderRadius: "4px",
backgroundColor: PRIMARY_COLOR,
}
@@ -1,26 +1,21 @@
// @ts-nocheck
import React from "react"
import { Img, Row, Html, Link, Body, Head, Button, Column, Preview, Section, Container } from "@jsx-email/all"
import { Hr, Text, Fonts, SplitString, Title, A, Span, B } from "../components"
import { Text, Fonts, Title, A, Span } from "../components"
import {
unit,
body,
code,
frame,
medium,
heading,
headingText,
container,
headingHr,
footerLink,
breadcrumb,
compactText,
buttonPrimary,
breadcrumbColonSeparator,
contentText,
button,
contentHighlightText,
linkText,
buttonText,
} from "../styles"
const LOCAL_ASSETS_URL = "/static"
const CONSOLE_URL = "https://opencode.ai/"
const DOC_URL = "https://opencode.ai/docs/zen"
interface InviteEmailProps {
inviter: string
@@ -32,9 +27,8 @@ export const InviteEmail = ({
inviter = "test@anoma.ly",
workspaceID = "wrk_01K6XFY7V53T8XN0A7X8G9BTN3",
workspaceName = "anomaly",
assetsUrl = LOCAL_ASSETS_URL,
assetsUrl = `${CONSOLE_URL}email`,
}: InviteEmailProps) => {
const subject = `You've been invited to join the ${workspaceName} workspace on OpenCode Console`
const messagePlain = `${inviter} invited you to join the ${workspaceName} workspace.`
const url = `${CONSOLE_URL}workspace/${workspaceID}`
return (
@@ -55,50 +49,29 @@ export const InviteEmail = ({
</Column>
</Row>
<Row style={headingHr}>
<Column>
<Hr />
</Column>
</Row>
<Section style={{ padding: `${unit}px 0 0 0` }}>
<Text style={{ ...compactText }}>
<B>{inviter}</B> invited you to join the{" "}
<Link style={medium} href={url}>
<B>{workspaceName}</B>
</Link>{" "}
workspace in the{" "}
<Link style={medium} href={`${CONSOLE_URL}zen`}>
OpenCode Console
</Link>
.
<Section style={{ padding: `${unit * 2}px 0 0 0` }}>
<Text style={headingText}>Join your team's OpenCode workspace</Text>
<Text style={contentText}>
You have been invited by <Span style={contentHighlightText}>{inviter}</Span> to join the{" "}
<Span style={contentHighlightText}>{workspaceName}</Span> workspace on OpenCode.
</Text>
</Section>
<Section style={{ padding: `${unit}px 0 0 0` }}>
<Button style={buttonPrimary} href={url}>
<Span style={code}>Join Workspace</Span>
<Button style={button} href={url}>
<Text style={buttonText}>
Join workspace
<Img width="24" height="24" src={`${assetsUrl}/right-arrow.png`} alt="Arrow right" />
</Text>
</Button>
</Section>
<Row style={headingHr}>
<Column>
<Hr />
</Column>
</Row>
<Row>
<Column>
<Link href={`${CONSOLE_URL}zen`} style={footerLink}>
Console
</Link>
</Column>
<Column align="right">
<Link style={footerLink} href={DOC_URL}>
About
</Link>
</Column>
</Row>
<Section style={{ padding: `${unit}px 0 0 0` }}>
<Text style={contentText}>Button not working? Copy the following link...</Text>
<Link href={url}>
<Text style={linkText}>{url}</Text>
</Link>
</Section>
</Section>
</Container>
</Body>
Binary file not shown.

After

Width:  |  Height:  |  Size: 308 B

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "0.15.0",
"version": "0.15.5",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-resource",
"dependencies": {
"@cloudflare/workers-types": "^4.20250830.0"
"@cloudflare/workers-types": "catalog:"
},
"exports": {
".": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-scripts",
"version": "0.15.0",
"version": "0.15.5",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
@@ -0,0 +1 @@
../../CLAUDE.md
+34
View File
@@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
+12
View File
@@ -0,0 +1,12 @@
{
"name": "@opencode-ai/css",
"version": "0.15.5",
"type": "module",
"exports": {
".": "./src/index.css",
"./*": "./src/*"
},
"scripts": {
"dev": "bun run dev.ts"
}
}
+393
View File
@@ -0,0 +1,393 @@
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Remove default margins and padding
3. Reset all borders.
*/
*,
::after,
::before,
::backdrop,
::file-selector-button {
box-sizing: border-box; /* 1 */
margin: 0; /* 2 */
padding: 0; /* 2 */
border: 0 solid; /* 3 */
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
7. Disable tap highlights on iOS.
*/
html,
:host {
line-height: 1.5; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
tab-size: 4; /* 3 */
font-family: --theme(
--default-font-family,
ui-sans-serif,
system-ui,
sans-serif,
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol",
"Noto Color Emoji"
); /* 4 */
font-feature-settings: --theme(--default-font-feature-settings, normal); /* 5 */
font-variation-settings: --theme(--default-font-variation-settings, normal); /* 6 */
-webkit-tap-highlight-color: transparent; /* 7 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Reset the default border style to a 1px solid border.
*/
hr {
height: 0; /* 1 */
color: inherit; /* 2 */
border-top-width: 1px; /* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
-webkit-text-decoration: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font-family by default.
2. Use the user's configured `mono` font-feature-settings by default.
3. Use the user's configured `mono` font-variation-settings by default.
4. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: --theme(
--default-mono-font-family,
ui-monospace,
SFMono-Regular,
Menlo,
Monaco,
Consolas,
"Liberation Mono",
"Courier New",
monospace
); /* 1 */
font-feature-settings: --theme(--default-mono-font-feature-settings, normal); /* 2 */
font-variation-settings: --theme(--default-mono-font-variation-settings, normal); /* 3 */
font-size: 1em; /* 4 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0; /* 1 */
border-color: inherit; /* 2 */
border-collapse: collapse; /* 3 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Make lists unstyled by default.
*/
ol,
ul,
menu {
list-style: none;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block; /* 1 */
vertical-align: middle; /* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/*
1. Inherit font styles in all browsers.
2. Remove border radius in all browsers.
3. Remove background color in all browsers.
4. Ensure consistent opacity for disabled states in all browsers.
*/
button,
input,
select,
optgroup,
textarea,
::file-selector-button {
font: inherit; /* 1 */
font-feature-settings: inherit; /* 1 */
font-variation-settings: inherit; /* 1 */
letter-spacing: inherit; /* 1 */
color: inherit; /* 1 */
border-radius: 0; /* 2 */
background-color: transparent; /* 3 */
opacity: 1; /* 4 */
}
/*
Restore default font weight.
*/
:where(select:is([multiple], [size])) optgroup {
font-weight: bolder;
}
/*
Restore indentation.
*/
:where(select:is([multiple], [size])) optgroup option {
padding-inline-start: 20px;
}
/*
Restore space after button.
*/
::file-selector-button {
margin-inline-end: 4px;
}
/*
Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
*/
::placeholder {
opacity: 1;
}
/*
Set the default placeholder color to a semi-transparent version of the current text color in browsers that do not
crash when using `color-mix(…)` with `currentcolor`. (https://github.com/tailwindlabs/tailwindcss/issues/17194)
*/
@supports (not (-webkit-appearance: -apple-pay-button)) /* Not Safari */ or (contain-intrinsic-size: 1px)
/* Safari 17+ */ {
::placeholder {
color: color-mix(in oklab, currentcolor 50%, transparent);
}
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Ensure date/time inputs have the same height when empty in iOS Safari.
2. Ensure text alignment can be changed on date/time inputs in iOS Safari.
*/
::-webkit-date-and-time-value {
min-height: 1lh; /* 1 */
text-align: inherit; /* 2 */
}
/*
Prevent height from changing on date/time inputs in macOS Safari when the input is set to `display: block`.
*/
::-webkit-datetime-edit {
display: inline-flex;
}
/*
Remove excess padding from pseudo-elements in date/time inputs to ensure consistent height across browsers.
*/
::-webkit-datetime-edit-fields-wrapper {
padding: 0;
}
::-webkit-datetime-edit,
::-webkit-datetime-edit-year-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-minute-field,
::-webkit-datetime-edit-second-field,
::-webkit-datetime-edit-millisecond-field,
::-webkit-datetime-edit-meridiem-field {
padding-block: 0;
}
/*
Center dropdown marker shown on inputs with paired `<datalist>`s in Chrome. (https://github.com/tailwindlabs/tailwindcss/issues/18499)
*/
::-webkit-calendar-picker-indicator {
line-height: 1;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Correct the inability to style the border radius in iOS Safari.
*/
button,
input:where([type="button"], [type="reset"], [type="submit"]),
::file-selector-button {
appearance: button;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
Make elements with the HTML hidden attribute stay hidden by default.
*/
[hidden]:where(:not([hidden="until-found"])) {
display: none !important;
}
+496
View File
@@ -0,0 +1,496 @@
:root {
--black: #000000;
--white: #ffffff;
--grey-dark-1: #ffffff;
--grey-dark-2: #ffffff;
--grey-dark-3: #ffffff;
--grey-dark-4: #ffffff;
--grey-dark-5: #ffffff;
--grey-dark-6: #ffffff;
--grey-dark-7: #ffffff;
--grey-dark-8: #ffffff;
--grey-dark-9: #ffffff;
--grey-dark-10: #ffffff;
--grey-dark-11: #ffffff;
--grey-dark-12: #ffffff;
--grey-light-1: #ffffff;
--grey-dark-alpha-1: #ffffff;
--grey-dark-alpha-2: #ffffff;
--grey-dark-alpha-3: #ffffff;
--grey-dark-alpha-4: #ffffff;
--grey-dark-alpha-5: #ffffff;
--grey-dark-alpha-6: #ffffff;
--grey-dark-alpha-7: #ffffff;
--grey-dark-alpha-8: #ffffff;
--grey-dark-alpha-9: #ffffff;
--grey-dark-alpha-10: #ffffff;
--grey-dark-alpha-11: #ffffff;
--grey-dark-alpha-12: #ffffff;
--smoke-dark-1: #131010;
--grey-light-2: #ffffff;
--grey-light-3: #ffffff;
--grey-light-4: #ffffff;
--grey-light-5: #ffffff;
--grey-light-6: #ffffff;
--grey-light-7: #ffffff;
--grey-light-8: #ffffff;
--grey-light-9: #ffffff;
--grey-light-10: #ffffff;
--grey-light-11: #ffffff;
--grey-light-12: #ffffff;
--smoke-dark-2: #1b1818;
--smoke-dark-3: #252121;
--smoke-dark-4: #2d2828;
--smoke-dark-5: #343030;
--smoke-dark-6: #3e3939;
--smoke-dark-7: #4b4646;
--smoke-dark-8: #645f5f;
--smoke-dark-9: #716c6b;
--smoke-dark-10: #7f7979;
--smoke-dark-11: #b7b1b1;
--smoke-dark-12: #f1ecec;
--smoke-light-1: #fdfcfc;
--smoke-light-2: #f9f8f8;
--smoke-light-3: #f1f0f0;
--smoke-light-4: #e9e8e8;
--smoke-light-5: #e2e0e0;
--smoke-light-6: #dad9d9;
--smoke-light-7: #cfcecd;
--smoke-light-8: #bcbbbb;
--smoke-light-9: #8e8b8b;
--smoke-light-10: #848181;
--smoke-light-11: #656363;
--smoke-light-12: #211e1e;
--smoke-dark-alpha-1: #bb000003;
--smoke-dark-alpha-2: #f9b4b40b;
--smoke-dark-alpha-3: #f9caca16;
--smoke-dark-alpha-4: #ffd5d51e;
--smoke-dark-alpha-5: #fce2e226;
--smoke-dark-alpha-6: #fce2e231;
--smoke-dark-alpha-7: #fce8e83f;
--smoke-dark-alpha-8: #fff1f159;
--smoke-dark-alpha-9: #fff3f067;
--smoke-dark-alpha-10: #fff2f276;
--smoke-dark-alpha-11: #fff7f7b2;
--smoke-dark-alpha-12: #fffafaf0;
--smoke-light-alpha-1: #55000003;
--smoke-light-alpha-2: #25000007;
--smoke-light-alpha-3: #1100000f;
--smoke-light-alpha-4: #0c000017;
--smoke-light-alpha-5: #1100001f;
--smoke-light-alpha-6: #07000026;
--smoke-light-alpha-7: #0b060032;
--smoke-light-alpha-8: #04000044;
--smoke-light-alpha-9: #07000074;
--smoke-light-alpha-10: #0400009c;
--smoke-light-alpha-11: #0700007e;
--smoke-light-alpha-12: #020000df;
--yuzu-dark-1: #11120c;
--yuzu-light-1: #fdfdfb;
--yuzu-light-2: #fbfceb;
--yuzu-light-3: #f8fac5;
--yuzu-light-4: #f2f4a5;
--yuzu-light-5: #e9eb9a;
--yuzu-light-6: #dcde8e;
--yuzu-light-7: #cccd7e;
--yuzu-light-8: #b6b768;
--yuzu-light-9: #dcde8d;
--yuzu-light-10: #d2d384;
--yuzu-light-11: #7c7c2c;
--yuzu-light-12: #3d3d23;
--yuzu-dark-2: #181810;
--yuzu-dark-3: #262614;
--yuzu-dark-4: #313115;
--yuzu-dark-5: #3d3d18;
--yuzu-dark-6: #4a4a21;
--yuzu-dark-7: #5a5b2c;
--yuzu-dark-8: #6f6f36;
--yuzu-dark-9: #fdffca;
--yuzu-dark-10: #f4f6c1;
--yuzu-dark-11: #dbdda0;
--yuzu-dark-12: #eff1bd;
--yuzu-dark-alpha-1: #11910002;
--yuzu-dark-alpha-2: #f1f10008;
--yuzu-dark-alpha-3: #fafa3317;
--yuzu-dark-alpha-4: #fbfb2f23;
--yuzu-dark-alpha-5: #fbfb3730;
--yuzu-dark-alpha-6: #fcfc533e;
--yuzu-dark-alpha-7: #fafd6750;
--yuzu-dark-alpha-8: #ffff6f65;
--yuzu-dark-alpha-9: #fdffca;
--yuzu-dark-alpha-10: #fcfec7f6;
--yuzu-dark-alpha-11: #fdffb8db;
--yuzu-dark-alpha-12: #fdffc8f0;
--yuzu-light-alpha-1: #80800004;
--yuzu-light-alpha-2: #ccd90014;
--yuzu-light-alpha-3: #e1ea003a;
--yuzu-light-alpha-4: #dbe0015a;
--yuzu-light-alpha-5: #c8cd0065;
--yuzu-light-alpha-6: #b1b50071;
--yuzu-light-alpha-7: #9b9d0081;
--yuzu-light-alpha-8: #84860097;
--yuzu-light-alpha-9: #b1b60072;
--yuzu-light-alpha-10: #a2a4017b;
--yuzu-light-alpha-11: #616100d3;
--yuzu-light-alpha-12: #1e1e00dc;
--cobalt-dark-1: #091120;
--cobalt-dark-2: #0d172b;
--cobalt-dark-3: #0c2255;
--cobalt-dark-4: #0c2a74;
--cobalt-dark-5: #113489;
--cobalt-dark-6: #18409b;
--cobalt-dark-7: #204cb1;
--cobalt-dark-8: #2558d0;
--cobalt-dark-9: #034cff;
--cobalt-dark-10: #0038ee;
--cobalt-dark-11: #89b5ff;
--cobalt-dark-12: #cde2ff;
--cobalt-light-1: #fcfdff;
--cobalt-light-2: #f5faff;
--cobalt-light-3: #eaf2ff;
--cobalt-light-4: #daeaff;
--cobalt-light-5: #c8e0ff;
--cobalt-light-6: #b4d2ff;
--cobalt-light-7: #98bfff;
--cobalt-dark-alpha-1: #0011f211;
--cobalt-dark-alpha-2: #0048fe1c;
--cobalt-dark-alpha-3: #004dff49;
--cobalt-dark-alpha-4: #064dfd6b;
--cobalt-dark-alpha-5: #1157ff81;
--cobalt-dark-alpha-6: #1e62ff94;
--cobalt-dark-alpha-7: #2768feac;
--cobalt-dark-alpha-8: #2a6affcd;
--cobalt-dark-alpha-9: #034cff;
--cobalt-dark-alpha-10: #003bffed;
--cobalt-dark-alpha-11: #89b5ff;
--cobalt-dark-alpha-12: #cde2ff;
--cobalt-light-8: #73a4ff;
--cobalt-light-9: #034cff;
--cobalt-light-10: #0443de;
--cobalt-light-11: #1251ec;
--cobalt-light-12: #0f2b6c;
--cobalt-light-alpha-1: #0055ff03;
--cobalt-light-alpha-2: #0080ff0a;
--cobalt-light-alpha-3: #0062ff15;
--cobalt-light-alpha-4: #006fff25;
--cobalt-light-alpha-5: #0070ff37;
--cobalt-light-alpha-6: #0167ff4b;
--cobalt-light-alpha-7: #0061ff67;
--cobalt-light-alpha-8: #005aff8c;
--cobalt-light-alpha-9: #004afffc;
--cobalt-light-alpha-10: #0040ddfb;
--cobalt-light-alpha-11: #0044ebed;
--cobalt-light-alpha-12: #001e63f0;
--apple-dark-1: #0c140b;
--apple-light-1: #fafefa;
--apple-light-2: #f4fcf3;
--apple-light-3: #e1fade;
--apple-light-4: #cef6c9;
--apple-light-5: #b9efb3;
--apple-light-6: #9fe598;
--apple-light-7: #7dd676;
--apple-light-8: #43c23b;
--apple-light-9: #12c905;
--apple-light-10: #00bd00;
--apple-light-11: #008600;
--apple-light-12: #184115;
--apple-dark-2: #121b11;
--apple-dark-3: #152d13;
--apple-dark-4: #123d0f;
--apple-dark-5: #174b14;
--apple-dark-6: #1d5b19;
--apple-dark-7: #226c1e;
--apple-dark-8: #267f20;
--apple-dark-9: #12c905;
--apple-dark-10: #17bb0d;
--apple-dark-11: #37db2e;
--apple-dark-12: #aff7a8;
--apple-dark-alpha-1: #00d10004;
--apple-dark-alpha-2: #29f9120b;
--apple-dark-alpha-3: #33ff221e;
--apple-dark-alpha-4: #17fb0730;
--apple-dark-alpha-5: #2afc1e3f;
--apple-dark-alpha-6: #37fd2b50;
--apple-dark-alpha-7: #3efe3362;
--apple-dark-alpha-8: #3fff3276;
--apple-dark-alpha-9: #12fe02c6;
--apple-dark-alpha-10: #19fe0cb7;
--apple-dark-alpha-11: #3dfe33d9;
--apple-dark-alpha-12: #b4feacf7;
--apple-light-alpha-1: #00cc0005;
--apple-light-alpha-2: #16c0000c;
--apple-light-alpha-3: #18d90021;
--apple-light-alpha-4: #18d50036;
--apple-light-alpha-5: #15ca004c;
--apple-light-alpha-6: #12bf0067;
--apple-light-alpha-7: #0db30089;
--apple-light-alpha-8: #0bb000c4;
--apple-light-alpha-9: #0dc800fa;
--apple-light-alpha-10: #00bd00;
--apple-light-alpha-11: #008600;
--apple-light-alpha-12: #033000ea;
--ember-dark-1: #170f0d;
--ember-dark-2: #201412;
--ember-dark-3: #3c140d;
--ember-dark-4: #530e05;
--ember-dark-5: #631409;
--ember-dark-6: #742216;
--ember-dark-7: #8d3324;
--ember-dark-8: #b64330;
--ember-dark-9: #fc533a;
--ember-dark-10: #ee462d;
--ember-dark-11: #ff917b;
--ember-dark-12: #ffd1c8;
--ember-light-1: #fffcfb;
--ember-light-2: #fff6f3;
--ember-light-3: #ffe9e4;
--ember-light-4: #ffd7cc;
--ember-light-5: #ffc8ba;
--ember-light-6: #ffb7a6;
--ember-light-7: #ffa392;
--ember-light-8: #f68975;
--ember-light-9: #fc533a;
--ember-light-10: #ef442a;
--ember-light-11: #da3319;
--ember-light-12: #5c281f;
--ember-dark-alpha-1: #ec000007;
--ember-dark-alpha-2: #f23e2011;
--ember-dark-alpha-3: #fb22002f;
--ember-dark-alpha-4: #ff070047;
--ember-dark-alpha-5: #ff1a0058;
--ember-dark-alpha-6: #fd3a1d6b;
--ember-dark-alpha-7: #ff533685;
--ember-dark-alpha-8: #ff5a3eb1;
--ember-dark-alpha-9: #ff553bfc;
--ember-dark-alpha-10: #ff4a2fed;
--ember-dark-alpha-11: #ff917b;
--ember-dark-alpha-12: #ffd1c8;
--ember-light-alpha-1: #ff400004;
--ember-light-alpha-2: #ff40000c;
--ember-light-alpha-3: #ff30001b;
--ember-light-alpha-4: #ff370033;
--ember-light-alpha-5: #ff340045;
--ember-light-alpha-6: #ff310059;
--ember-light-alpha-7: #ff28006d;
--ember-light-alpha-8: #ef25008a;
--ember-light-alpha-9: #fb2200c5;
--ember-light-alpha-10: #ec1f00d5;
--ember-light-alpha-11: #d61d00e6;
--ember-light-alpha-12: #460a00e0;
--solaris-dark-1: #13110b;
--solaris-dark-2: #1b180f;
--solaris-dark-3: #2a2307;
--solaris-dark-4: #382b00;
--solaris-dark-5: #443500;
--solaris-dark-6: #514307;
--solaris-dark-7: #64551a;
--solaris-dark-8: #7f6c25;
--solaris-dark-9: #fcd53a;
--solaris-dark-10: #f2cb2a;
--solaris-dark-11: #fdd63c;
--solaris-dark-12: #faebb5;
--solaris-light-1: #fefdfa;
--solaris-light-2: #fffbea;
--solaris-light-3: #fff6be;
--solaris-light-4: #ffee9c;
--solaris-light-5: #ffe47c;
--solaris-light-6: #f2d775;
--solaris-light-7: #e0c76f;
--solaris-light-8: #cdb047;
--solaris-light-9: #ffdc17;
--solaris-light-10: #fad337;
--solaris-light-11: #917500;
--solaris-light-12: #433c22;
--solaris-dark-alpha-1: #bb110003;
--solaris-dark-alpha-2: #f9b4000b;
--solaris-dark-alpha-3: #febb001b;
--solaris-dark-alpha-4: #feaf002a;
--solaris-dark-alpha-5: #feb80037;
--solaris-dark-alpha-6: #feca0045;
--solaris-dark-alpha-7: #ffd42b59;
--solaris-dark-alpha-8: #ffd63d76;
--solaris-dark-alpha-9: #ffd83bfc;
--solaris-dark-alpha-10: #fed52bf2;
--solaris-dark-alpha-11: #ffd83cfd;
--solaris-dark-alpha-12: #fff0b9fa;
--solaris-light-alpha-1: #cc990005;
--solaris-light-alpha-2: #ffcf0015;
--solaris-light-alpha-3: #ffdc0041;
--solaris-light-alpha-4: #ffd40063;
--solaris-light-alpha-5: #ffcb0083;
--solaris-light-alpha-6: #e7b6008a;
--solaris-light-alpha-7: #c89c0090;
--solaris-light-alpha-8: #ba9200b8;
--solaris-light-alpha-9: #ffd900e8;
--solaris-light-alpha-10: #f9c700c8;
--solaris-light-alpha-11: #917500;
--solaris-light-alpha-12: #261e00dd;
--lilac-dark-1: #140f14;
--lilac-dark-2: #1d141d;
--lilac-dark-3: #2f1e31;
--lilac-dark-4: #3e2440;
--lilac-dark-5: #4a2c4c;
--lilac-dark-6: #573859;
--lilac-dark-7: #6c486e;
--lilac-dark-8: #8a5e8d;
--lilac-dark-9: #edb2f1;
--lilac-dark-10: #e2a8e6;
--lilac-dark-11: #dca2e0;
--lilac-dark-12: #edd8ef;
--lilac-light-1: #fffcff;
--lilac-light-2: #fdf7fe;
--lilac-light-3: #fceafd;
--lilac-light-4: #faddfb;
--lilac-light-5: #f5cff7;
--lilac-light-6: #eebff1;
--lilac-light-7: #e3a9e7;
--lilac-light-8: #d78bdd;
--lilac-light-9: #a753ae;
--lilac-light-10: #9946a0;
--lilac-light-11: #95429c;
--lilac-light-12: #590b60;
--lilac-dark-alpha-1: #d100d104;
--lilac-dark-alpha-2: #fd4cfd0d;
--lilac-dark-alpha-3: #ec70fb23;
--lilac-dark-alpha-4: #f270fc33;
--lilac-dark-alpha-5: #f57dfd40;
--lilac-dark-alpha-6: #f691fd4e;
--lilac-dark-alpha-7: #fa9eff64;
--lilac-dark-alpha-8: #f9a5ff85;
--lilac-dark-alpha-9: #fbbcfff0;
--lilac-dark-alpha-10: #f9b9fee5;
--lilac-dark-alpha-11: #fab8ffde;
--lilac-dark-alpha-12: #fde6ffee;
--lilac-light-alpha-1: #ff00ff03;
--lilac-light-alpha-2: #c000e008;
--lilac-light-alpha-3: #db00e715;
--lilac-light-alpha-4: #da00e122;
--lilac-light-alpha-5: #ca00d530;
--lilac-light-alpha-6: #bc00c840;
--lilac-light-alpha-7: #ac00b856;
--lilac-light-alpha-8: #a800b574;
--lilac-light-alpha-9: #7d0087ac;
--lilac-light-alpha-10: #73007cb9;
--lilac-light-alpha-11: #70007abd;
--lilac-light-alpha-12: #520059f4;
--coral-dark-1: #160f0e;
--coral-light-1: #fffcfc;
--coral-light-2: #fff8f7;
--coral-light-3: #ffebe8;
--coral-light-4: #ffdbd5;
--coral-light-5: #ffcdc5;
--coral-light-6: #f9beb5;
--coral-light-7: #e9aea6;
--coral-light-8: #d49b93;
--coral-light-9: #af7871;
--coral-light-10: #a26c65;
--coral-light-11: #9c665f;
--coral-light-12: #592a24;
--coral-dark-2: #1f1413;
--coral-dark-3: #391613;
--coral-dark-4: #481b17;
--coral-dark-5: #542621;
--coral-dark-6: #63332d;
--coral-dark-7: #77453f;
--coral-dark-8: #935e57;
--coral-dark-9: #ffd6d0;
--coral-dark-10: #f5ccc6;
--coral-dark-11: #e2a8a0;
--coral-dark-12: #fcd3cd;
--coral-dark-alpha-1: #e6000006;
--coral-dark-alpha-2: #ff44330f;
--coral-dark-alpha-3: #ff2f1d2b;
--coral-dark-alpha-4: #ff3d2b3b;
--coral-dark-alpha-5: #ff5c4a48;
--coral-dark-alpha-6: #ff746358;
--coral-dark-alpha-7: #fd897c6e;
--coral-dark-alpha-8: #fe9d908c;
--coral-dark-alpha-9: #ffd6d0;
--coral-dark-alpha-10: #fed3cdf5;
--coral-dark-alpha-11: #ffbdb4e0;
--coral-dark-alpha-12: #ffd6cffc;
--coral-light-alpha-1: #ff000003;
--coral-light-alpha-2: #ff200008;
--coral-light-alpha-3: #ff220017;
--coral-light-alpha-4: #ff25002a;
--coral-light-alpha-5: #ff24003a;
--coral-light-alpha-6: #eb20014a;
--coral-light-alpha-7: #c0170059;
--coral-light-alpha-8: #9a13006c;
--coral-light-alpha-9: #700d008e;
--coral-light-alpha-10: #650c009a;
--coral-light-alpha-11: #620b00a0;
--coral-light-alpha-12: #3e0700db;
--mint-dark-alpha-1: #00bb0003;
--mint-dark-alpha-2: #2bf72b0a;
--mint-dark-alpha-3: #66fe5d1b;
--mint-dark-alpha-4: #63ff5d2c;
--mint-dark-alpha-5: #6cff643b;
--mint-dark-alpha-6: #71ff6a4b;
--mint-dark-alpha-7: #74fd6f5d;
--mint-dark-alpha-8: #74ff6f72;
--mint-dark-alpha-9: #c8ffc4f5;
--mint-dark-alpha-10: #c6fec2f5;
--mint-dark-alpha-11: #b4ffafdc;
--mint-dark-alpha-12: #c7ffc3fb;
--mint-dark-1: #0d130c;
--mint-dark-2: #121a12;
--mint-dark-3: #1a2a19;
--mint-dark-4: #1f3a1e;
--mint-dark-5: #264824;
--mint-dark-6: #2d572b;
--mint-dark-7: #356733;
--mint-dark-8: #3d7b3b;
--mint-dark-9: #c8ffc4;
--mint-dark-10: #bff5bb;
--mint-dark-11: #9dde99;
--mint-dark-12: #c4fbc0;
--mint-light-1: #fafefa;
--mint-light-2: #f4fcf3;
--mint-light-3: #dbfdd8;
--mint-light-4: #c3fabf;
--mint-light-5: #adf2a8;
--mint-light-6: #96e692;
--mint-light-7: #81d47d;
--mint-light-8: #6abc67;
--mint-light-9: #9ff29a;
--mint-light-10: #98e793;
--mint-light-11: #318430;
--mint-light-12: #1f461d;
--mint-dark-alpha-1: #00bb0003;
--mint-dark-alpha-2: #2bf72b09;
--mint-dark-alpha-3: #66fe5d1b;
--mint-dark-alpha-4: #63ff5d2b;
--mint-dark-alpha-5: #6cff643b;
--mint-dark-alpha-6: #71ff6a4a;
--mint-dark-alpha-7: #74fd6f5c;
--mint-dark-alpha-8: #74ff6f72;
--mint-dark-alpha-9: #c8ffc4f5;
--mint-dark-alpha-10: #c6fec2f5;
--mint-dark-alpha-11: #b4ffafdb;
--mint-dark-alpha-12: #c7ffc3fa;
--mint-light-alpha-1: #00cc0005;
--mint-light-alpha-2: #16c0000c;
--mint-light-alpha-3: #14f20027;
--mint-light-alpha-4: #10ec0040;
--mint-light-alpha-5: #0fd90057;
--mint-light-alpha-6: #0ac5006d;
--mint-light-alpha-7: #08ab0082;
--mint-light-alpha-8: #058f0098;
--mint-light-alpha-9: #0ddf0065;
--mint-light-alpha-10: #0cc7006c;
--mint-light-alpha-11: #016800cf;
--mint-light-alpha-12: #022e00e2;
--grey-light-alpha-1: #ffffff;
--grey-light-alpha-2: #ffffff;
--grey-light-alpha-3: #ffffff;
--grey-light-alpha-4: #ffffff;
--grey-light-alpha-5: #ffffff;
--grey-light-alpha-6: #ffffff;
--grey-light-alpha-7: #ffffff;
--grey-light-alpha-8: #ffffff;
--grey-light-alpha-9: #ffffff;
--grey-light-alpha-10: #ffffff;
--grey-light-alpha-11: #ffffff;
--grey-light-alpha-12: #ffffff;
}
+93
View File
@@ -0,0 +1,93 @@
[data-component="button"] {
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
border-style: solid;
border-width: 1px;
border-radius: var(--radius-md);
font-family: var(--font-sans);
font-size: var(--text-base);
line-height: var(--text-base--line-height);
font-weight: var(--font-weight-normal);
text-decoration: none;
user-select: none;
gap: calc(var(--spacing) * 2);
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
&:focus {
outline: none;
}
&[data-variant="primary"] {
border-color: var(--border-default-border);
background-color: var(--surface-brand-surface-brand);
color: var(--text-on-brand-text-strong-on-brand);
&:hover:not(:disabled) {
border-color: var(--border-default-border-hover);
background-color: var(--surface-brand-surface-brand-hover);
}
&:active:not(:disabled) {
border-color: var(--border-default-border-active);
background-color: var(--surface-brand-surface-brand-active);
}
&:focus:not(:disabled) {
border-color: var(--border-default-border-focus);
background-color: var(--surface-brand-surface-brand-focus);
}
}
&[data-variant="secondary"] {
border-color: var(--border-default-border);
background-color: var(--surface-default-surface);
color: var(--text-default-text);
&:hover:not(:disabled) {
border-color: var(--border-default-border-hover);
background-color: var(--surface-default-surface-hover);
}
&:active:not(:disabled) {
border-color: var(--border-default-border-active);
background-color: var(--surface-default-surface-active);
}
&:focus:not(:disabled) {
border-color: var(--border-default-border-focus);
background-color: var(--surface-default-surface-focus);
}
}
&[data-variant="ghost"] {
border-color: transparent;
background-color: transparent;
color: var(--text-default-text);
&:hover:not(:disabled) {
background-color: var(--surface-default-surface-hover);
}
&:active:not(:disabled) {
border-color: var(--border-default-border-active);
background-color: var(--surface-default-surface-active);
}
&:focus:not(:disabled) {
border-color: var(--border-default-border-focus);
background-color: var(--surface-default-surface-focus);
}
}
&[data-size="normal"] {
padding: calc(var(--spacing) * 0.5) calc(var(--spacing) * 3);
font-size: var(--text-xs);
line-height: var(--text-sm--line-height);
}
&[data-size="large"] {
padding: calc(var(--spacing) * 1) calc(var(--spacing) * 4);
font-size: var(--text-sm);
line-height: var(--text-sm--line-height);
}
}
+6
View File
@@ -0,0 +1,6 @@
[data-component="icon"] {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
+120
View File
@@ -0,0 +1,120 @@
[data-component="select"] {
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
&:focus {
outline: none;
box-shadow: 0 0 0 2px var(--color-primary);
}
[data-slot="section"] {
font-size: var(--text-xs);
line-height: var(--text-xs--line-height);
font-weight: var(--font-weight-light);
text-transform: uppercase;
color: var(--text-default-text-weak);
opacity: 0.6;
margin-top: calc(var(--spacing) * 3);
margin-left: calc(var(--spacing) * 2);
&:first-child {
margin-top: 0;
}
}
[data-slot="item"] {
position: relative;
display: flex;
align-items: center;
padding: calc(var(--spacing) * 2) calc(var(--spacing) * 2);
border-radius: var(--radius-sm);
font-size: var(--text-xs);
line-height: var(--text-xs--line-height);
color: var(--text-default-text);
cursor: pointer;
transition:
background-color 0.2s ease-in-out,
color 0.2s ease-in-out;
outline: none;
user-select: none;
&[data-highlighted] {
background-color: var(--surface-default-surface);
}
&[data-disabled] {
pointer-events: none;
opacity: 0.5;
}
[data-slot="item-indicator"] {
margin-left: auto;
}
}
[data-slot="trigger"] {
[data-slot="value"] {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
[data-slot="icon"] {
width: fit-content;
height: fit-content;
flex-shrink: 0;
color: var(--text-default-text-weak);
transition: transform 0.1s ease-in-out;
}
}
}
[data-component="select-content"] {
min-width: 8rem;
overflow: hidden;
border-radius: var(--radius-md);
border-width: 1px;
border-style: solid;
border-color: var(--border-default-border-weak);
background-color: var(--surface-raised-surface-raised);
padding: calc(var(--spacing) * 1);
box-shadow: var(--shadow-md);
z-index: 50;
&[data-closed] {
animation: select-close 0.15s ease-out;
}
&[data-expanded] {
animation: select-open 0.15s ease-out;
}
[data-slot="list"] {
overflow-y: auto;
max-height: 12rem;
white-space: nowrap;
overflow-x: hidden;
}
}
@keyframes select-open {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes select-close {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.95);
}
}
+98
View File
@@ -0,0 +1,98 @@
[data-component="tabs"] {
display: flex;
flex-direction: column;
height: 100%;
& [data-slot="list"] {
position: relative;
display: flex;
align-items: center;
background-color: var(--surface-default-surface);
overflow-x: auto;
/* Hide scrollbar */
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
/* Divider between tabs */
& > [data-slot="trigger"]:not(:first-child) {
border-left: 1px solid var(--border-default-border-weak);
}
/* After element to fill remaining space */
&::after {
content: "";
display: block;
flex-grow: 1;
height: calc(var(--spacing) * 8);
border-left: 1px solid var(--border-default-border-weak);
border-bottom: 1px solid var(--border-default-border-weak);
}
&:empty::after {
border-left: none;
}
}
& [data-slot="trigger"] {
position: relative;
padding: 0 calc(var(--spacing) * 3);
height: calc(var(--spacing) * 8);
display: flex;
align-items: center;
font-size: var(--text-sm);
font-weight: var(--font-weight-medium);
color: var(--text-default-text-weak);
cursor: pointer;
white-space: nowrap;
flex-shrink: 0;
border-bottom: 1px solid var(--border-default-border-weak);
background-color: transparent;
transition:
background-color 0.15s ease,
color 0.15s ease;
&:disabled {
pointer-events: none;
opacity: 0.5;
}
&:focus-visible {
outline: none;
box-shadow: 0 0 0 2px var(--border-default-border-focus);
}
&[data-selected] {
color: var(--text-default-text);
background-color: var(--surface-panel-surface);
border-bottom-color: transparent;
}
&:hover:not(:disabled):not([data-selected]) {
color: var(--text-default-text);
}
}
& [data-slot="content"] {
background-color: var(--surface-panel-surface);
overflow-y: auto;
flex: 1;
/* Hide scrollbar */
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
&:focus-visible {
outline: none;
box-shadow: 0 0 0 2px var(--border-default-border-focus);
}
}
}
+13
View File
@@ -0,0 +1,13 @@
@layer theme, base, components, utilities;
@import "./color.css" layer(theme);
@import "./theme.css" layer(theme);
@import "./base.css" layer(base);
@import "./components/button.css" layer(components);
@import "./components/icon.css" layer(components);
@import "./components/select.css" layer(components);
@import "./components/tabs.css" layer(components);
@import "./utilities.css" layer(utilities);
+536
View File
@@ -0,0 +1,536 @@
:root {
--font-sans:
ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--spacing: 0.25rem;
--breakpoint-sm: 40rem;
--breakpoint-md: 48rem;
--breakpoint-lg: 64rem;
--breakpoint-xl: 80rem;
--breakpoint-2xl: 96rem;
--container-3xs: 16rem;
--container-2xs: 18rem;
--container-xs: 20rem;
--container-sm: 24rem;
--container-md: 28rem;
--container-lg: 32rem;
--container-xl: 36rem;
--container-2xl: 42rem;
--container-3xl: 48rem;
--container-4xl: 56rem;
--container-5xl: 64rem;
--container-6xl: 72rem;
--container-7xl: 80rem;
--text-xs: 0.75rem;
--text-xs--line-height: calc(1 / 0.75);
--text-sm: 0.875rem;
--text-sm--line-height: calc(1.25 / 0.875);
--text-base: 1rem;
--text-base--line-height: calc(1.5 / 1);
--text-lg: 1.125rem;
--text-lg--line-height: calc(1.75 / 1.125);
--text-xl: 1.25rem;
--text-xl--line-height: calc(1.75 / 1.25);
--text-2xl: 1.5rem;
--text-2xl--line-height: calc(2 / 1.5);
--text-3xl: 1.875rem;
--text-3xl--line-height: calc(2.25 / 1.875);
--text-4xl: 2.25rem;
--text-4xl--line-height: calc(2.5 / 2.25);
--text-5xl: 3rem;
--text-5xl--line-height: 1;
--text-6xl: 3.75rem;
--text-6xl--line-height: 1;
--text-7xl: 4.5rem;
--text-7xl--line-height: 1;
--text-8xl: 6rem;
--text-8xl--line-height: 1;
--text-9xl: 8rem;
--text-9xl--line-height: 1;
--font-weight-thin: 100;
--font-weight-extralight: 200;
--font-weight-light: 300;
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--font-weight-extrabold: 800;
--font-weight-black: 900;
--tracking-tighter: -0.05em;
--tracking-tight: -0.025em;
--tracking-normal: 0em;
--tracking-wide: 0.025em;
--tracking-wider: 0.05em;
--tracking-widest: 0.1em;
--leading-tight: 1.25;
--leading-snug: 1.375;
--leading-normal: 1.5;
--leading-relaxed: 1.625;
--leading-loose: 2;
--radius-xs: 0.125rem;
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--radius-2xl: 1rem;
--radius-3xl: 1.5rem;
--radius-4xl: 2rem;
--shadow-2xs: 0 1px rgb(0 0 0 / 0.05);
--shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
--shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
--inset-shadow-2xs: inset 0 1px rgb(0 0 0 / 0.05);
--inset-shadow-xs: inset 0 1px 1px rgb(0 0 0 / 0.05);
--inset-shadow-sm: inset 0 2px 4px rgb(0 0 0 / 0.05);
--drop-shadow-xs: 0 1px 1px rgb(0 0 0 / 0.05);
--drop-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.15);
--drop-shadow-md: 0 3px 3px rgb(0 0 0 / 0.12);
--drop-shadow-lg: 0 4px 4px rgb(0 0 0 / 0.15);
--drop-shadow-xl: 0 9px 7px rgb(0 0 0 / 0.1);
--drop-shadow-2xl: 0 25px 25px rgb(0 0 0 / 0.15);
--text-shadow-2xs: 0px 1px 0px rgb(0 0 0 / 0.15);
--text-shadow-xs: 0px 1px 1px rgb(0 0 0 / 0.2);
--text-shadow-sm: 0px 1px 0px rgb(0 0 0 / 0.075), 0px 1px 1px rgb(0 0 0 / 0.075), 0px 2px 2px rgb(0 0 0 / 0.075);
--text-shadow-md: 0px 1px 1px rgb(0 0 0 / 0.1), 0px 1px 2px rgb(0 0 0 / 0.1), 0px 2px 4px rgb(0 0 0 / 0.1);
--text-shadow-lg: 0px 1px 2px rgb(0 0 0 / 0.1), 0px 3px 2px rgb(0 0 0 / 0.1), 0px 4px 8px rgb(0 0 0 / 0.1);
}
:root {
/* OC-1-Light */
color-scheme: light;
--background-background: var(--smoke-light-1);
--background-background-weak: #f7f6f6;
--background-background-weaker: #fcfcfc;
--surface-default-surface: var(--smoke-light-2);
--input-input: var(--smoke-light-1);
--input-input-hover: var(--smoke-light-2);
--input-input-active: var(--cobalt-light-1);
--input-input-selected: var(--cobalt-light-4);
--input-input-focus: var(--cobalt-light-1);
--input-input-disabled: var(--smoke-light-4);
--surface-default-surface-hover: var(--smoke-light-4);
--surface-default-surface-active: var(--smoke-light-5);
--surface-default-surface-selected: var(--cobalt-light-3);
--surface-default-surface-disabled: var(--smoke-light-2);
--surface-default-surface-focus: var(--cobalt-light-3);
--surface-inset-surface-inset: var(--smoke-light-3);
--surface-inset-surface-inset-hover: var(--smoke-light-4);
--surface-inset-surface-inset-active: var(--smoke-light-6);
--surface-inset-surface-inset-selected: var(--cobalt-light-4);
--surface-inset-surface-inset-disabled: var(--smoke-light-3);
--surface-inset-surface-inset-focus: var(--cobalt-light-4);
--surface-raised-surface-raised: var(--white);
--surface-raised-surface-raised-hover: var(--smoke-light-3);
--surface-raised-surface-raised-active: var(--smoke-light-4);
--surface-raised-surface-raised-selected: var(--cobalt-light-3);
--surface-raised-surface-raised-disabled: var(--smoke-light-2);
--surface-raised-surface-raised-focus: var(--cobalt-light-3);
--surface-overlay-surface-overlay: var(--smoke-light-alpha-9);
--surface-brand-surface-brand: var(--yuzu-light-9);
--surface-brand-surface-brand-hover: var(--yuzu-light-10);
--surface-interactive-surface-interactive: var(--cobalt-light-3);
--surface-interactive-surface-interactive-hover: var(--cobalt-light-4);
--surface-interactive-surface-interactive-weak: var(--cobalt-light-2);
--surface-interactive-surface-interactive-weak-hover: var(--cobalt-light-3);
--surface-success-surface-success: var(--apple-light-3);
--surface-success-surface-success-weak: var(--apple-light-2);
--surface-success-surface-success-strong: var(--apple-light-9);
--surface-warning-surface-warning: var(--solaris-light-3);
--surface-warning-surface-warning-weak: var(--solaris-light-2);
--surface-warning-surface-warning-strong: var(--solaris-light-9);
--surface-critical-surface-critical: var(--ember-light-3);
--surface-critical-surface-critical-weak: var(--ember-light-2);
--surface-critical-surface-critical-strong: var(--ember-light-9);
--surface-info-surface-info: var(--lilac-light-3);
--surface-info-surface-info-weak: var(--lilac-light-2);
--surface-info-surface-info-strong: var(--lilac-light-9);
--surface-diff-add-surface-diff-add: var(--coral-light-3);
--surface-diff-add-surface-diff-add-weak: var(--coral-light-2);
--surface-diff-add-surface-diff-add-weaker: var(--coral-light-1);
--surface-diff-add-surface-diff-add-strong: var(--coral-light-9);
--surface-diff-add-surface-diff-add-stronger: var(--coral-light-12);
--surface-diff-delete-surface-diff-delete: var(--mint-light-3);
--surface-on-background-weak-default-surface-on-background-weak: var(--smoke-light-1);
--surface-on-background-weak-inset-surface-inset-on-background-weak: var(--smoke-light-1);
--surface-on-background-weak-default-surface-on-background-weak-hover: var(--smoke-light-1);
--surface-on-background-weak-default-surface-on-background-weak-active: var(--smoke-light-1);
--surface-on-background-weak-default-surface-on-background-weak-selected: var(--smoke-light-1);
--surface-on-background-weak-default-surface-on-background-weak-disabled: var(--smoke-light-1);
--surface-on-background-weak-default-surface-on-background-weak-focus: var(--smoke-light-1);
--surface-on-background-weak-inset-surface-inset-on-background-weak-hover: var(--smoke-light-1);
--surface-on-background-weak-inset-surface-inset-on-background-weak-active: var(--smoke-light-1);
--surface-on-background-weak-inset-surface-inset-on-background-weak-selected: var(--smoke-light-1);
--surface-on-background-weak-inset-surface-inset-on-background-weak-disabled: var(--smoke-light-1);
--surface-on-background-weak-inset-surface-inset-on-background-weak-focus: var(--smoke-light-1);
--surface-diff-delete-surface-diff-delete-weak: var(--mint-light-2);
--surface-diff-delete-surface-diff-delete-weaker: var(--mint-light-1);
--surface-diff-delete-surface-diff-delete-strong: var(--mint-light-9);
--surface-diff-delete-surface-diff-delete-stronger: var(--mint-light-11);
--text-default-text: var(--smoke-light-11);
--text-default-text-weak: var(--smoke-light-9);
--text-default-text-weaker: var(--smoke-light-8);
--text-default-text-strong: var(--smoke-light-12);
--text-on-brand-text-on-brand: var(--smoke-light-alpha-11);
--text-on-interactive-text-on-interactive: var(--smoke-light-alpha-11);
--text-on-success-text-on-success: var(--smoke-dark-alpha-11);
--text-on-warning-text-on-warning: var(--smoke-dark-alpha-11);
--text-on-info-text-on-info: var(--smoke-dark-alpha-11);
--text-on-diff-add-text-on-diff-add: var(--smoke-dark-alpha-11);
--text-on-diff-delete-text-on-diff-delete: var(--smoke-dark-alpha-11);
--text-on-diff-delete-text-weak-on-diff-delete: var(--smoke-dark-alpha-9);
--text-on-diff-delete-text-strong-on-diff-delete: var(--smoke-dark-alpha-12);
--text-on-diff-add-text-weak-on-diff-add: var(--smoke-dark-alpha-9);
--text-on-diff-add-text-strong-on-diff-add: var(--smoke-dark-alpha-12);
--text-on-info-text-weak-on-info: var(--smoke-dark-alpha-9);
--text-on-info-text-strong-on-info: var(--smoke-dark-alpha-12);
--text-on-warning-text-weak-on-warning: var(--smoke-dark-alpha-9);
--text-on-warning-text-strong-on-warning: var(--smoke-dark-alpha-12);
--text-on-success-text-weak-on-success: var(--smoke-dark-alpha-9);
--text-on-success-text-strong-on-success: var(--smoke-dark-alpha-12);
--text-on-interactive-text-weak-on-interactive: var(--smoke-light-alpha-9);
--text-on-interactive-text-strong-on-interactive: var(--smoke-light-alpha-12);
--text-on-brand-text-weak-on-brand: var(--smoke-light-alpha-9);
--text-on-brand-text-weaker-on-brand: var(--smoke-light-alpha-8);
--text-on-brand-text-strong-on-brand: var(--smoke-light-alpha-12);
--border-default-border: var(--smoke-light-alpha-7);
--border-default-border-hover: var(--smoke-light-alpha-8);
--border-default-border-active: var(--smoke-light-alpha-9);
--border-default-border-selected: var(--cobalt-light-alpha-9);
--border-default-border-disabled: var(--smoke-light-alpha-8);
--border-default-border-focus: var(--smoke-light-alpha-9);
--border-default-border-weak: var(--smoke-light-alpha-5);
--icon-default-icon: var(--smoke-light-9);
--border-default-border-weak-hover: var(--smoke-light-alpha-6);
--icon-default-icon-hover: var(--smoke-light-10);
--border-default-border-weak-active: var(--smoke-light-alpha-7);
--icon-default-icon-active: var(--smoke-light-11);
--icon-default-icon-selected: var(--smoke-light-12);
--icon-default-icon-disabled: var(--smoke-light-7);
--icon-default-icon-focus: var(--smoke-light-12);
--icon-default-icon-weak: var(--smoke-light-7);
--icon-default-icon-weak-hover: var(--smoke-light-8);
--icon-default-icon-weak-active: var(--smoke-light-9);
--icon-default-icon-weak-selected: var(--smoke-light-10);
--icon-default-icon-weak-disabled: var(--smoke-light-6);
--icon-default-icon-weak-focus: var(--smoke-light-9);
--icon-default-icon-strong: var(--smoke-light-12);
--icon-default-icon-strong-hover: var(--smoke-light-12);
--icon-default-icon-strong-active: var(--smoke-light-12);
--icon-default-icon-strong-selected: var(--smoke-light-12);
--icon-default-icon-strong-disabled: var(--smoke-light-8);
--icon-default-icon-strong-focus: var(--smoke-light-12);
--border-default-border-weak-selected: var(--cobalt-light-alpha-6);
--border-default-border-weak-disabled: var(--smoke-light-alpha-6);
--border-default-border-weak-focus: var(--smoke-light-alpha-7);
--border-interactive-border-interactive: var(--cobalt-light-7);
--border-interactive-border-interactive-hover: var(--cobalt-light-8);
--border-interactive-border-interactive-active: var(--cobalt-light-9);
--border-interactive-border-interactive-selected: var(--cobalt-light-9);
--border-interactive-border-interactive-disabled: var(--smoke-light-8);
--border-interactive-border-interactive-focus: var(--cobalt-light-9);
--border-success-border-success: var(--apple-light-6);
--border-success-border-success-hover: var(--apple-light-7);
--border-success-border-success-selected: var(--apple-light-9);
--border-warning-border-warning: var(--solaris-light-6);
--border-warning-border-warning-hover: var(--solaris-light-7);
--border-warning-border-warning-selected: var(--solaris-light-9);
--border-critical-border-critical: var(--ember-light-6);
--border-critical-border-critical-hover: var(--ember-light-7);
--border-critical-border-critical-selected: var(--ember-light-9);
--border-info-border-info: var(--lilac-light-6);
--border-info-border-info-hover: var(--lilac-light-7);
--border-info-border-info-selected: var(--lilac-light-9);
--icon-brand-icon-brand: var(--smoke-light-12);
--icon-success-icon-success: var(--apple-light-7);
--icon-warning-icon-warning: var(--solaris-light-7);
--icon-warning-icon-warning-hover: var(--solaris-light-8);
--icon-warning-icon-warning-active: var(--solaris-light-11);
--icon-success-icon-success-hover: var(--apple-light-8);
--icon-success-icon-success-active: var(--apple-light-11);
--icon-critical-icon-critical: var(--ember-light-7);
--icon-critical-icon-critical-hover: var(--ember-light-8);
--icon-critical-icon-critical-active: var(--ember-light-11);
--icon-info-icon-info: var(--lilac-light-7);
--icon-info-icon-info-hover: var(--lilac-light-8);
--icon-info-icon-info-active: var(--lilac-light-11);
--icon-on-brand-icon-on-brand: var(--smoke-light-alpha-11);
--icon-on-brand-icon-on-brand-hover: var(--smoke-light-alpha-12);
--icon-on-brand-icon-on-brand-selected: var(--smoke-light-alpha-12);
--icon-on-interactive-icon-on-interactive: var(--smoke-light-alpha-9);
--icon-on-interactive-icon-on-interactive-hover: var(--smoke-light-alpha-10);
--icon-on-interactive-icon-on-interactive-selected: var(--smoke-light-alpha-11);
--icon-on-success-icon-on-success: var(--apple-light-alpha-9);
--icon-on-success-icon-on-success-hover: var(--apple-light-alpha-10);
--icon-on-success-icon-on-success-selected: var(--apple-light-alpha-11);
--icon-on-warning-icon-on-warning: var(--solaris-light-alpha-9);
--icon-on-warning-icon-on-warning-hover: var(--solaris-light-alpha-10);
--icon-on-warning-icon-on-warning-selected: var(--solaris-light-alpha-11);
--icon-on-critical-icon-on-critical: var(--ember-light-alpha-9);
--icon-on-critical-icon-on-critical-hover: var(--ember-light-alpha-10);
--icon-on-critical-icon-on-critical-selected: var(--ember-light-alpha-11);
--icon-on-info-icon-on-info: var(--lilac-light-9);
--icon-on-diff-add-icon-on-diff-add: var(--mint-dark-alpha-9);
--icon-on-diff-delete-icon-on-diff-delete: var(--coral-light-alpha-9);
--icon-on-diff-delete-icon-on-diff-delete-hover: var(--coral-light-alpha-10);
--icon-on-diff-delete-icon-on-diff-delete-active: var(--coral-light-alpha-11);
--icon-on-diff-add-icon-on-diff-add-hover: var(--mint-dark-alpha-10);
--icon-on-diff-add-icon-on-diff-add-active: var(--mint-dark-alpha-11);
--icon-on-info-icon-on-info-hover: var(--lilac-light-alpha-10);
--icon-on-info-icon-on-info-selected: var(--lilac-light-alpha-11);
--syntax-syntax-comment: #ffffff;
--syntax-syntax-string: #ffffff;
--syntax-syntax-keyword: #ffffff;
--syntax-syntax-function: #ffffff;
--syntax-syntax-number: #ffffff;
--syntax-syntax-operator: #ffffff;
--syntax-syntax-variable: #ffffff;
--syntax-syntax-type: #ffffff;
--syntax-syntax-constant: #ffffff;
--syntax-syntax-punctuation: #ffffff;
--syntax-syntax-success: #ffffff;
--syntax-syntax-warning: #ffffff;
--syntax-syntax-critical: #ffffff;
--syntax-syntax-info: #ffffff;
--markdown-markdown-heading: #ffffff;
--markdown-markdown-text: #ffffff;
--markdown-markdown-link: #ffffff;
--markdown-markdown-link-text: #ffffff;
--markdown-markdown-code: #ffffff;
--markdown-markdown-block-quote: #ffffff;
--markdown-markdown-emph: #ffffff;
--markdown-markdown-strong: #ffffff;
--markdown-markdown-horizontal-rule: #ffffff;
--markdown-markdown-list-item: #ffffff;
--markdown-markdown-list-enumeration: #ffffff;
--markdown-markdown-image: #ffffff;
--markdown-markdown-image-text: #ffffff;
--markdown-markdown-code-block: #ffffff;
.dark {
/* OC-1-Dark */
color-scheme: dark;
--background-background: var(--smoke-dark-1);
--background-background-weak: #151313;
--background-background-weaker: #201c1c;
--surface-default-surface: var(--smoke-dark-3);
--input-input: var(--smoke-dark-2);
--input-input-hover: var(--smoke-dark-2);
--input-input-active: var(--cobalt-dark-1);
--input-input-selected: var(--cobalt-dark-2);
--input-input-focus: var(--cobalt-dark-1);
--input-input-disabled: var(--smoke-dark-4);
--surface-default-surface-hover: var(--smoke-dark-4);
--surface-default-surface-active: var(--smoke-dark-5);
--surface-default-surface-selected: var(--cobalt-dark-3);
--surface-default-surface-disabled: var(--smoke-dark-2);
--surface-default-surface-focus: var(--cobalt-dark-3);
--surface-inset-surface-inset: var(--smoke-dark-4);
--surface-inset-surface-inset-hover: var(--smoke-dark-5);
--surface-inset-surface-inset-active: var(--smoke-dark-6);
--surface-inset-surface-inset-selected: var(--cobalt-dark-4);
--surface-inset-surface-inset-disabled: var(--smoke-dark-3);
--surface-inset-surface-inset-focus: var(--cobalt-dark-4);
--surface-raised-surface-raised: var(--smoke-dark-4);
--surface-raised-surface-raised-hover: var(--smoke-dark-7);
--surface-raised-surface-raised-active: var(--smoke-dark-8);
--surface-raised-surface-raised-selected: var(--cobalt-dark-3);
--surface-raised-surface-raised-disabled: var(--smoke-dark-3);
--surface-raised-surface-raised-focus: var(--cobalt-dark-3);
--surface-overlay-surface-overlay: var(--smoke-dark-alpha-2);
--surface-brand-surface-brand: var(--yuzu-light-9);
--surface-brand-surface-brand-hover: var(--yuzu-light-10);
--surface-interactive-surface-interactive: var(--cobalt-light-3);
--surface-interactive-surface-interactive-hover: var(--cobalt-light-4);
--surface-interactive-surface-interactive-weak: var(--cobalt-light-2);
--surface-interactive-surface-interactive-weak-hover: var(--cobalt-light-3);
--surface-success-surface-success: var(--apple-light-3);
--surface-success-surface-success-weak: var(--apple-light-2);
--surface-success-surface-success-strong: var(--apple-light-9);
--surface-warning-surface-warning: var(--solaris-light-3);
--surface-warning-surface-warning-weak: var(--solaris-light-2);
--surface-warning-surface-warning-strong: var(--solaris-light-9);
--surface-critical-surface-critical: var(--ember-light-3);
--surface-critical-surface-critical-weak: var(--ember-light-2);
--surface-critical-surface-critical-strong: var(--ember-light-9);
--surface-info-surface-info: var(--lilac-light-3);
--surface-info-surface-info-weak: var(--lilac-light-2);
--surface-info-surface-info-strong: var(--lilac-light-9);
--surface-diff-add-surface-diff-add: var(--coral-light-3);
--surface-diff-add-surface-diff-add-weak: var(--coral-light-2);
--surface-diff-add-surface-diff-add-weaker: var(--coral-light-1);
--surface-diff-add-surface-diff-add-strong: var(--coral-light-9);
--surface-diff-add-surface-diff-add-stronger: var(--coral-light-12);
--surface-diff-delete-surface-diff-delete: var(--mint-light-3);
--surface-on-background-weak-default-surface-on-background-weak: var(--smoke-dark-1);
--surface-on-background-weak-inset-surface-inset-on-background-weak: var(--smoke-light-1);
--surface-on-background-weak-default-surface-on-background-weak-hover: var(--smoke-light-1);
--surface-on-background-weak-default-surface-on-background-weak-active: var(--smoke-light-1);
--surface-on-background-weak-default-surface-on-background-weak-selected: var(--smoke-light-1);
--surface-on-background-weak-default-surface-on-background-weak-disabled: var(--smoke-light-1);
--surface-on-background-weak-default-surface-on-background-weak-focus: var(--smoke-light-1);
--surface-on-background-weak-inset-surface-inset-on-background-weak-hover: var(--smoke-light-1);
--surface-on-background-weak-inset-surface-inset-on-background-weak-active: var(--smoke-light-1);
--surface-on-background-weak-inset-surface-inset-on-background-weak-selected: var(--smoke-light-1);
--surface-on-background-weak-inset-surface-inset-on-background-weak-disabled: var(--smoke-light-1);
--surface-on-background-weak-inset-surface-inset-on-background-weak-focus: var(--smoke-light-1);
--surface-diff-delete-surface-diff-delete-weak: var(--mint-light-2);
--surface-diff-delete-surface-diff-delete-weaker: var(--mint-light-1);
--surface-diff-delete-surface-diff-delete-strong: var(--mint-light-9);
--surface-diff-delete-surface-diff-delete-stronger: var(--mint-light-11);
--text-default-text: var(--smoke-dark-alpha-11);
--text-default-text-weak: var(--smoke-dark-alpha-9);
--text-default-text-weaker: var(--smoke-dark-alpha-8);
--text-default-text-strong: var(--smoke-dark-alpha-12);
--text-on-brand-text-on-brand: var(--smoke-dark-alpha-11);
--text-on-interactive-text-on-interactive: var(--smoke-dark-alpha-11);
--text-on-success-text-on-success: var(--smoke-dark-alpha-11);
--text-on-warning-text-on-warning: var(--smoke-dark-alpha-11);
--text-on-info-text-on-info: var(--smoke-dark-alpha-11);
--text-on-diff-add-text-on-diff-add: var(--smoke-dark-alpha-11);
--text-on-diff-delete-text-on-diff-delete: var(--smoke-dark-alpha-11);
--text-on-diff-delete-text-weak-on-diff-delete: var(--smoke-dark-alpha-9);
--text-on-diff-delete-text-strong-on-diff-delete: var(--smoke-dark-alpha-12);
--text-on-diff-add-text-weak-on-diff-add: var(--smoke-dark-alpha-9);
--text-on-diff-add-text-strong-on-diff-add: var(--smoke-dark-alpha-12);
--text-on-info-text-weak-on-info: var(--smoke-dark-alpha-9);
--text-on-info-text-strong-on-info: var(--smoke-dark-alpha-12);
--text-on-warning-text-weak-on-warning: var(--smoke-dark-alpha-9);
--text-on-warning-text-strong-on-warning: var(--smoke-dark-alpha-12);
--text-on-success-text-weak-on-success: var(--smoke-dark-alpha-9);
--text-on-success-text-strong-on-success: var(--smoke-dark-alpha-12);
--text-on-interactive-text-weak-on-interactive: var(--smoke-dark-alpha-9);
--text-on-interactive-text-strong-on-interactive: var(--smoke-dark-alpha-12);
--text-on-brand-text-weak-on-brand: var(--smoke-dark-alpha-9);
--text-on-brand-text-weaker-on-brand: var(--smoke-dark-alpha-8);
--text-on-brand-text-strong-on-brand: var(--smoke-dark-alpha-12);
--border-default-border: var(--smoke-dark-alpha-7);
--border-default-border-hover: var(--smoke-dark-alpha-8);
--border-default-border-active: var(--smoke-dark-alpha-9);
--border-default-border-selected: var(--cobalt-dark-alpha-9);
--border-default-border-disabled: var(--smoke-dark-alpha-8);
--border-default-border-focus: var(--smoke-dark-alpha-9);
--border-default-border-weak: var(--smoke-dark-alpha-6);
--icon-default-icon: var(--smoke-light-9);
--border-default-border-weak-hover: var(--smoke-dark-alpha-7);
--icon-default-icon-hover: var(--smoke-light-10);
--border-default-border-weak-active: var(--smoke-dark-alpha-8);
--icon-default-icon-active: var(--smoke-light-11);
--icon-default-icon-selected: var(--smoke-light-12);
--icon-default-icon-disabled: var(--smoke-light-7);
--icon-default-icon-focus: var(--smoke-light-12);
--icon-default-icon-weak: var(--smoke-dark-6);
--icon-default-icon-weak-hover: var(--smoke-light-7);
--icon-default-icon-weak-active: var(--smoke-light-8);
--icon-default-icon-weak-selected: var(--smoke-light-9);
--icon-default-icon-weak-disabled: var(--smoke-light-4);
--icon-default-icon-weak-focus: var(--smoke-light-9);
--icon-default-icon-strong: var(--smoke-dark-12);
--icon-default-icon-strong-hover: var(--smoke-light-12);
--icon-default-icon-strong-active: var(--smoke-light-12);
--icon-default-icon-strong-selected: var(--smoke-light-12);
--icon-default-icon-strong-disabled: var(--smoke-light-8);
--icon-default-icon-strong-focus: var(--smoke-light-12);
--border-default-border-weak-selected: var(--cobalt-dark-alpha-6);
--border-default-border-weak-disabled: var(--smoke-dark-alpha-6);
--border-default-border-weak-focus: var(--smoke-dark-alpha-8);
--border-interactive-border-interactive: var(--cobalt-light-7);
--border-interactive-border-interactive-hover: var(--cobalt-light-8);
--border-interactive-border-interactive-active: var(--cobalt-light-9);
--border-interactive-border-interactive-selected: var(--cobalt-light-9);
--border-interactive-border-interactive-disabled: var(--smoke-light-8);
--border-interactive-border-interactive-focus: var(--cobalt-light-9);
--border-success-border-success: var(--apple-light-6);
--border-success-border-success-hover: var(--apple-light-7);
--border-success-border-success-selected: var(--apple-light-9);
--border-warning-border-warning: var(--solaris-light-6);
--border-warning-border-warning-hover: var(--solaris-light-7);
--border-warning-border-warning-selected: var(--solaris-light-9);
--border-critical-border-critical: var(--ember-light-6);
--border-critical-border-critical-hover: var(--ember-light-7);
--border-critical-border-critical-selected: var(--ember-light-9);
--border-info-border-info: var(--lilac-light-6);
--border-info-border-info-hover: var(--lilac-light-7);
--border-info-border-info-selected: var(--lilac-light-9);
--icon-brand-icon-brand: var(--white);
--icon-success-icon-success: var(--apple-light-7);
--icon-warning-icon-warning: var(--solaris-light-7);
--icon-warning-icon-warning-hover: var(--solaris-light-8);
--icon-warning-icon-warning-active: var(--solaris-light-11);
--icon-success-icon-success-hover: var(--apple-light-8);
--icon-success-icon-success-active: var(--apple-light-11);
--icon-critical-icon-critical: var(--ember-light-7);
--icon-critical-icon-critical-hover: var(--ember-light-8);
--icon-critical-icon-critical-active: var(--ember-light-11);
--icon-info-icon-info: var(--lilac-light-7);
--icon-info-icon-info-hover: var(--lilac-light-8);
--icon-info-icon-info-active: var(--lilac-light-11);
--icon-on-brand-icon-on-brand: var(--smoke-light-alpha-11);
--icon-on-brand-icon-on-brand-hover: var(--smoke-light-alpha-12);
--icon-on-brand-icon-on-brand-selected: var(--smoke-light-alpha-12);
--icon-on-interactive-icon-on-interactive: var(--smoke-dark-alpha-9);
--icon-on-interactive-icon-on-interactive-hover: var(--smoke-dark-alpha-10);
--icon-on-interactive-icon-on-interactive-selected: var(--smoke-dark-alpha-11);
--icon-on-success-icon-on-success: var(--apple-light-alpha-9);
--icon-on-success-icon-on-success-hover: var(--apple-light-alpha-10);
--icon-on-success-icon-on-success-selected: var(--apple-light-alpha-11);
--icon-on-warning-icon-on-warning: var(--solaris-light-alpha-9);
--icon-on-warning-icon-on-warning-hover: var(--solaris-light-alpha-10);
--icon-on-warning-icon-on-warning-selected: var(--solaris-light-alpha-11);
--icon-on-critical-icon-on-critical: var(--ember-light-alpha-9);
--icon-on-critical-icon-on-critical-hover: var(--ember-light-alpha-10);
--icon-on-critical-icon-on-critical-selected: var(--ember-light-alpha-11);
--icon-on-info-icon-on-info: var(--lilac-light-9);
--icon-on-diff-add-icon-on-diff-add: var(--mint-dark-alpha-9);
--icon-on-diff-delete-icon-on-diff-delete: var(--coral-light-alpha-9);
--icon-on-diff-delete-icon-on-diff-delete-hover: var(--coral-light-alpha-10);
--icon-on-diff-delete-icon-on-diff-delete-active: var(--coral-light-alpha-11);
--icon-on-diff-add-icon-on-diff-add-hover: var(--mint-dark-alpha-10);
--icon-on-diff-add-icon-on-diff-add-active: var(--mint-dark-alpha-11);
--icon-on-info-icon-on-info-hover: var(--lilac-light-alpha-10);
--icon-on-info-icon-on-info-selected: var(--lilac-light-alpha-11);
--syntax-syntax-comment: #ffffff;
--syntax-syntax-string: #ffffff;
--syntax-syntax-keyword: #ffffff;
--syntax-syntax-function: #ffffff;
--syntax-syntax-number: #ffffff;
--syntax-syntax-operator: #ffffff;
--syntax-syntax-variable: #ffffff;
--syntax-syntax-type: #ffffff;
--syntax-syntax-constant: #ffffff;
--syntax-syntax-punctuation: #ffffff;
--syntax-syntax-success: #ffffff;
--syntax-syntax-warning: #ffffff;
--syntax-syntax-critical: #ffffff;
--syntax-syntax-info: #ffffff;
--markdown-markdown-heading: #ffffff;
--markdown-markdown-text: #ffffff;
--markdown-markdown-link: #ffffff;
--markdown-markdown-link-text: #ffffff;
--markdown-markdown-code: #ffffff;
--markdown-markdown-block-quote: #ffffff;
--markdown-markdown-emph: #ffffff;
--markdown-markdown-strong: #ffffff;
--markdown-markdown-horizontal-rule: #ffffff;
--markdown-markdown-list-item: #ffffff;
--markdown-markdown-list-enumeration: #ffffff;
--markdown-markdown-image: #ffffff;
--markdown-markdown-image-text: #ffffff;
--markdown-markdown-code-block: #ffffff;
}
}
+14
View File
@@ -0,0 +1,14 @@
:root {
interpolate-size: allow-keywords;
}
.no-scrollbar {
&::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
& {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
}
+9 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/desktop",
"version": "0.15.0",
"version": "0.15.5",
"description": "",
"type": "module",
"scripts": {
@@ -8,18 +8,19 @@
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"typecheck": "tsc --noEmit"
"typecheck": "tsgo --noEmit"
},
"license": "MIT",
"devDependencies": {
"@tailwindcss/vite": "4.1.11",
"@tailwindcss/vite": "catalog:",
"@tsconfig/bun": "1.0.9",
"@types/luxon": "3.7.1",
"@types/node": "catalog:",
"typescript": "catalog:",
"vite": "^6.0.0",
"@typescript/native-preview": "catalog:",
"vite": "catalog:",
"vite-plugin-icons-spritesheet": "3.0.1",
"vite-plugin-solid": "^2.11.6"
"vite-plugin-solid": "catalog:"
},
"dependencies": {
"@kobalte/core": "catalog:",
@@ -30,7 +31,8 @@
"@solid-primitives/scroll": "2.1.3",
"@solidjs/router": "0.15.3",
"@thisbeyond/solid-dnd": "0.7.5",
"diff": "8.0.2",
"diff": "catalog:",
"@opencode-ai/ui": "workspace:*",
"fuzzysort": "catalog:",
"luxon": "catalog:",
"marked": "16.2.0",
@@ -39,7 +41,7 @@
"solid-js": "catalog:",
"shiki": "3.9.2",
"solid-list": "0.3.0",
"tailwindcss": "4.1.11",
"tailwindcss": "catalog:",
"virtua": "0.42.3"
},
"prettier": {
+2 -111
View File
@@ -13,30 +13,16 @@ import {
import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
import type { LocalFile } from "@/context/local"
import { Code } from "@/components/code"
import PromptForm from "@/components/prompt-form"
import { useLocal, useSDK, useSync } from "@/context"
import { getFilename } from "@/utils"
import { useLocal } from "@/context"
import type { JSX } from "solid-js"
interface EditorPaneProps {
layoutKey: string
timelinePane: string
onFileClick: (file: LocalFile) => void
onOpenModelSelect: () => void
onInputRefChange: (element: HTMLTextAreaElement | null) => void
}
export default function EditorPane(props: EditorPaneProps): JSX.Element {
const [localProps] = splitProps(props, [
"layoutKey",
"timelinePane",
"onFileClick",
"onOpenModelSelect",
"onInputRefChange",
])
const [localProps] = splitProps(props, ["onFileClick"])
const local = useLocal()
const sdk = useSDK()
const sync = useSync()
const [activeItem, setActiveItem] = createSignal<string | undefined>(undefined)
const navigateChange = (dir: 1 | -1) => {
@@ -55,73 +41,6 @@ export default function EditorPane(props: EditorPaneProps): JSX.Element {
local.file.close(file.path)
}
const handlePromptSubmit = async (prompt: string) => {
const existingSession = local.layout.visible(localProps.layoutKey, localProps.timelinePane)
? local.session.active()
: undefined
let session = existingSession
if (!session) {
const created = await sdk.session.create()
session = created.data ?? undefined
}
if (!session) return
local.session.setActive(session.id)
local.layout.show(localProps.layoutKey, localProps.timelinePane)
await sdk.session.prompt({
path: { id: session.id },
body: {
agent: local.agent.current()!.name,
model: {
modelID: local.model.current()!.id,
providerID: local.model.current()!.provider.id,
},
parts: [
{
type: "text",
text: prompt,
},
...(local.context.active()
? [
{
type: "file" as const,
mime: "text/plain",
url: `file://${local.context.active()!.absolute}`,
filename: local.context.active()!.name,
source: {
type: "file" as const,
text: {
value: "@" + local.context.active()!.name,
start: 0,
end: 0,
},
path: local.context.active()!.absolute,
},
},
]
: []),
...local.context.all().flatMap((file) => [
{
type: "file" as const,
mime: "text/plain",
url: `file://${sync.absolute(file.path)}${file.selection ? `?start=${file.selection.startLine}&end=${file.selection.endLine}` : ""}`,
filename: getFilename(file.path),
source: {
type: "file" as const,
text: {
value: "@" + getFilename(file.path),
start: 0,
end: 0,
},
path: sync.absolute(file.path),
},
},
]),
],
},
})
}
const handleDragStart = (event: unknown) => {
const id = getDraggableId(event)
if (!id) return
@@ -146,7 +65,6 @@ export default function EditorPane(props: EditorPaneProps): JSX.Element {
return (
<div class="relative flex h-full flex-col">
<Logo size={64} variant="ornate" class="absolute top-2/5 left-1/2 transform -translate-x-1/2 -translate-y-1/2" />
<DragDropProvider
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
@@ -237,23 +155,6 @@ export default function EditorPane(props: EditorPaneProps): JSX.Element {
)
})()}
</Show>
<Tooltip
value={local.layout.visible(localProps.layoutKey, localProps.timelinePane) ? "Close pane" : "Open pane"}
placement="bottom"
>
<IconButton
size="xs"
variant="ghost"
onClick={() => local.layout.toggle(localProps.layoutKey, localProps.timelinePane)}
>
<Icon
name={
local.layout.visible(localProps.layoutKey, localProps.timelinePane) ? "close-pane" : "open-pane"
}
size={14}
/>
</IconButton>
</Tooltip>
</div>
</div>
<For each={local.file.opened()}>
@@ -283,16 +184,6 @@ export default function EditorPane(props: EditorPaneProps): JSX.Element {
})()}
</DragOverlay>
</DragDropProvider>
<PromptForm
class="peer/editor absolute inset-x-4 z-50 flex items-center justify-center"
classList={{
"bottom-8": !!local.file.active(),
"bottom-3/8": local.file.active() === undefined,
}}
onSubmit={handlePromptSubmit}
onOpenModelSelect={localProps.onOpenModelSelect}
onInputRefChange={(element) => localProps.onInputRefChange(element ?? null)}
/>
</div>
)
}
@@ -0,0 +1,164 @@
import type { TextSelection } from "@/context/local"
import { getFilename } from "@/utils"
export interface PromptTextPart {
kind: "text"
value: string
}
export interface PromptAttachmentPart {
kind: "attachment"
token: string
display: string
path: string
selection?: TextSelection
origin: "context" | "active"
}
export interface PromptInterimPart {
kind: "interim"
value: string
leadingSpace: boolean
}
export type PromptContentPart = PromptTextPart | PromptAttachmentPart
export type PromptDisplaySegment =
| { kind: "text"; value: string }
| { kind: "attachment"; part: PromptAttachmentPart; source: string }
| PromptInterimPart
export interface AttachmentCandidate {
origin: "context" | "active"
path: string
selection?: TextSelection
display: string
}
export interface PromptSubmitValue {
text: string
parts: PromptContentPart[]
}
export const mentionPattern = /@([A-Za-z0-9_\-./]+)/g
export const mentionTriggerPattern = /(^|\s)@([A-Za-z0-9_\-./]*)$/
export type PromptSegment = (PromptTextPart | PromptAttachmentPart) & {
start: number
end: number
}
export type PromptAttachmentSegment = PromptAttachmentPart & {
start: number
end: number
}
function pushTextPart(parts: PromptContentPart[], value: string) {
if (!value) return
const last = parts[parts.length - 1]
if (last && last.kind === "text") {
last.value += value
return
}
parts.push({ kind: "text", value })
}
function addTextSegment(segments: PromptSegment[], start: number, value: string) {
if (!value) return
segments.push({ kind: "text", value, start, end: start + value.length })
}
export function createAttachmentDisplay(path: string, selection?: TextSelection) {
const base = getFilename(path)
if (!selection) return base
return `${base} (${selection.startLine}-${selection.endLine})`
}
export function registerCandidate(
map: Map<string, AttachmentCandidate>,
candidate: AttachmentCandidate,
tokens: (string | undefined)[],
) {
for (const token of tokens) {
if (!token) continue
const normalized = token.toLowerCase()
if (map.has(normalized)) continue
map.set(normalized, candidate)
}
}
export function parsePrompt(value: string, lookup: Map<string, AttachmentCandidate>) {
const segments: PromptSegment[] = []
if (!value) return { parts: [] as PromptContentPart[], segments }
const pushTextRange = (rangeStart: number, rangeEnd: number) => {
if (rangeEnd <= rangeStart) return
const text = value.slice(rangeStart, rangeEnd)
let cursor = 0
for (const match of text.matchAll(mentionPattern)) {
const localIndex = match.index ?? 0
if (localIndex > cursor) {
addTextSegment(segments, rangeStart + cursor, text.slice(cursor, localIndex))
}
const token = match[1]
const candidate = lookup.get(token.toLowerCase())
if (candidate) {
const start = rangeStart + localIndex
const end = start + match[0].length
segments.push({
kind: "attachment",
token,
display: candidate.display,
path: candidate.path,
selection: candidate.selection,
origin: candidate.origin,
start,
end,
})
} else {
addTextSegment(segments, rangeStart + localIndex, match[0])
}
cursor = localIndex + match[0].length
}
if (cursor < text.length) {
addTextSegment(segments, rangeStart + cursor, text.slice(cursor))
}
}
pushTextRange(0, value.length)
const parts: PromptContentPart[] = []
for (const segment of segments) {
if (segment.kind === "text") {
pushTextPart(parts, segment.value)
} else {
const { start, end, ...attachment } = segment
parts.push(attachment as PromptAttachmentPart)
}
}
return { parts, segments }
}
export function composeDisplaySegments(
segments: PromptSegment[],
inputValue: string,
interim: string,
): PromptDisplaySegment[] {
if (segments.length === 0 && !interim) return []
const display: PromptDisplaySegment[] = segments.map((segment) => {
if (segment.kind === "text") {
return { kind: "text", value: segment.value }
}
const { start, end, ...part } = segment
const placeholder = inputValue.slice(start, end)
return { kind: "attachment", part: part as PromptAttachmentPart, source: placeholder }
})
if (interim) {
const leadingSpace = !!(inputValue && !inputValue.endsWith(" ") && !interim.startsWith(" "))
display.push({ kind: "interim", value: interim, leadingSpace })
}
return display
}
@@ -0,0 +1,396 @@
import { createEffect, createMemo, createResource, type Accessor } from "solid-js"
import type { SetStoreFunction } from "solid-js/store"
import { getDirectory, getFilename } from "@/utils"
import { createSpeechRecognition } from "@/utils/speech"
import {
createAttachmentDisplay,
mentionPattern,
mentionTriggerPattern,
type PromptAttachmentPart,
type PromptAttachmentSegment,
} from "./prompt-form-helpers"
import type { LocalFile, TextSelection } from "@/context/local"
export type MentionRange = {
start: number
end: number
}
export interface PromptFormState {
promptInput: string
isDragOver: boolean
mentionOpen: boolean
mentionQuery: string
mentionRange: MentionRange | undefined
mentionIndex: number
mentionAnchorOffset: { x: number; y: number }
inlineAliases: Map<string, PromptAttachmentPart>
}
interface MentionControllerOptions {
state: PromptFormState
setState: SetStoreFunction<PromptFormState>
attachmentSegments: Accessor<PromptAttachmentSegment[]>
getInputRef: () => HTMLTextAreaElement | undefined
getOverlayRef: () => HTMLDivElement | undefined
getMeasureRef: () => HTMLDivElement | undefined
searchFiles: (query: string) => Promise<string[]>
resolveFile: (path: string) => LocalFile | undefined
addContextFile: (path: string, selection?: TextSelection) => void
getActiveContext: () => { path: string; selection?: TextSelection } | undefined
}
interface MentionKeyDownOptions {
event: KeyboardEvent & { currentTarget: HTMLTextAreaElement }
mentionItems: () => string[]
insertMention: (path: string) => void
}
interface ScrollSyncOptions {
state: PromptFormState
getInputRef: () => HTMLTextAreaElement | undefined
getOverlayRef: () => HTMLDivElement | undefined
interim: Accessor<string>
updateMentionPosition: (element: HTMLTextAreaElement, range?: MentionRange) => void
}
export function usePromptSpeech(updatePromptInput: (updater: (prev: string) => string) => void) {
return createSpeechRecognition({
onFinal: (text) => updatePromptInput((prev) => (prev && !prev.endsWith(" ") ? `${prev} ` : prev) + text),
})
}
export function useMentionController(options: MentionControllerOptions) {
const mentionSource = createMemo(() => (options.state.mentionOpen ? options.state.mentionQuery : undefined))
const [mentionResults, { mutate: mutateMentionResults }] = createResource(mentionSource, (query) => {
if (!options.state.mentionOpen) return []
return options.searchFiles(query ?? "")
})
const mentionItems = createMemo(() => mentionResults() ?? [])
createEffect(() => {
if (!options.state.mentionOpen) return
options.state.mentionQuery
options.setState("mentionIndex", 0)
})
createEffect(() => {
if (!options.state.mentionOpen) return
queueMicrotask(() => {
const input = options.getInputRef()
if (!input) return
if (document.activeElement === input) return
input.focus()
})
})
createEffect(() => {
const used = new Set<string>()
for (const match of options.state.promptInput.matchAll(mentionPattern)) {
const token = match[1]
if (token) used.add(token.toLowerCase())
}
options.setState("inlineAliases", (prev) => {
if (prev.size === 0) return prev
const next = new Map(prev)
let changed = false
for (const key of prev.keys()) {
if (!used.has(key.toLowerCase())) {
next.delete(key)
changed = true
}
}
return changed ? next : prev
})
})
createEffect(() => {
if (!options.state.mentionOpen) return
const items = mentionItems()
if (items.length === 0) {
options.setState("mentionIndex", 0)
return
}
if (options.state.mentionIndex < items.length) return
options.setState("mentionIndex", items.length - 1)
})
createEffect(() => {
if (!options.state.mentionOpen) return
const rangeValue = options.state.mentionRange
if (!rangeValue) return
options.state.promptInput
queueMicrotask(() => {
const input = options.getInputRef()
if (!input) return
updateMentionPosition(input, rangeValue)
})
})
function closeMention() {
if (options.state.mentionOpen) options.setState("mentionOpen", false)
options.setState("mentionQuery", "")
options.setState("mentionRange", undefined)
options.setState("mentionIndex", 0)
mutateMentionResults(() => undefined)
options.setState("mentionAnchorOffset", { x: 0, y: 0 })
}
function updateMentionPosition(element: HTMLTextAreaElement, rangeValue = options.state.mentionRange) {
const measure = options.getMeasureRef()
if (!measure) return
if (!rangeValue) return
measure.style.width = `${element.clientWidth}px`
const measurement = element.value.slice(0, rangeValue.end)
measure.textContent = measurement
const caretSpan = document.createElement("span")
caretSpan.textContent = "\u200b"
measure.append(caretSpan)
const caretRect = caretSpan.getBoundingClientRect()
const containerRect = measure.getBoundingClientRect()
measure.removeChild(caretSpan)
const left = caretRect.left - containerRect.left
const top = caretRect.top - containerRect.top - element.scrollTop
options.setState("mentionAnchorOffset", { x: left, y: top < 0 ? 0 : top })
}
function isValidMentionQuery(value: string) {
return /^[A-Za-z0-9_\-./]*$/.test(value)
}
function syncMentionFromCaret(element: HTMLTextAreaElement) {
if (!options.state.mentionOpen) return
const rangeValue = options.state.mentionRange
if (!rangeValue) {
closeMention()
return
}
const caret = element.selectionEnd ?? element.selectionStart ?? element.value.length
if (rangeValue.start < 0 || rangeValue.start >= element.value.length) {
closeMention()
return
}
if (element.value[rangeValue.start] !== "@") {
closeMention()
return
}
if (caret <= rangeValue.start) {
closeMention()
return
}
const mentionValue = element.value.slice(rangeValue.start + 1, caret)
if (!isValidMentionQuery(mentionValue)) {
closeMention()
return
}
options.setState("mentionRange", { start: rangeValue.start, end: caret })
options.setState("mentionQuery", mentionValue)
updateMentionPosition(element, { start: rangeValue.start, end: caret })
}
function tryOpenMentionFromCaret(element: HTMLTextAreaElement) {
const selectionStart = element.selectionStart ?? element.value.length
const selectionEnd = element.selectionEnd ?? selectionStart
if (selectionStart !== selectionEnd) return false
const caret = selectionEnd
if (options.attachmentSegments().some((segment) => caret >= segment.start && caret <= segment.end)) {
return false
}
const before = element.value.slice(0, caret)
const match = before.match(mentionTriggerPattern)
if (!match) return false
const token = match[2] ?? ""
const start = caret - token.length - 1
if (start < 0) return false
options.setState("mentionOpen", true)
options.setState("mentionRange", { start, end: caret })
options.setState("mentionQuery", token)
options.setState("mentionIndex", 0)
queueMicrotask(() => {
updateMentionPosition(element, { start, end: caret })
})
return true
}
function handlePromptInput(event: InputEvent & { currentTarget: HTMLTextAreaElement }) {
const element = event.currentTarget
options.setState("promptInput", element.value)
if (options.state.mentionOpen) {
syncMentionFromCaret(element)
if (options.state.mentionOpen) return
}
const isDeletion = event.inputType ? event.inputType.startsWith("delete") : false
if (!isDeletion && tryOpenMentionFromCaret(element)) return
closeMention()
}
function handleMentionKeyDown({ event, mentionItems: items, insertMention }: MentionKeyDownOptions) {
if (!options.state.mentionOpen) return false
const list = items()
if (event.key === "ArrowDown") {
event.preventDefault()
if (list.length === 0) return true
const next = options.state.mentionIndex + 1 >= list.length ? 0 : options.state.mentionIndex + 1
options.setState("mentionIndex", next)
return true
}
if (event.key === "ArrowUp") {
event.preventDefault()
if (list.length === 0) return true
const previous = options.state.mentionIndex - 1 < 0 ? list.length - 1 : options.state.mentionIndex - 1
options.setState("mentionIndex", previous)
return true
}
if (event.key === "Enter") {
event.preventDefault()
const targetItem = list[options.state.mentionIndex] ?? list[0]
if (targetItem) insertMention(targetItem)
return true
}
if (event.key === "Escape") {
event.preventDefault()
closeMention()
return true
}
return false
}
function generateMentionAlias(path: string) {
const existing = new Set<string>()
for (const key of options.state.inlineAliases.keys()) {
existing.add(key.toLowerCase())
}
for (const match of options.state.promptInput.matchAll(mentionPattern)) {
const token = match[1]
if (token) existing.add(token.toLowerCase())
}
const base = getFilename(path)
if (base) {
if (!existing.has(base.toLowerCase())) return base
}
const directory = getDirectory(path)
if (base && directory) {
const segments = directory.split("/").filter(Boolean)
for (let i = segments.length - 1; i >= 0; i -= 1) {
const candidate = `${segments.slice(i).join("/")}/${base}`
if (!existing.has(candidate.toLowerCase())) return candidate
}
}
if (!existing.has(path.toLowerCase())) return path
const fallback = base || path || "file"
let index = 2
let candidate = `${fallback}-${index}`
while (existing.has(candidate.toLowerCase())) {
index += 1
candidate = `${fallback}-${index}`
}
return candidate
}
function insertMention(path: string) {
const input = options.getInputRef()
if (!input) return
const rangeValue = options.state.mentionRange
if (!rangeValue) return
const node = options.resolveFile(path)
const alias = generateMentionAlias(path)
const mentionText = `@${alias}`
const value = options.state.promptInput
const before = value.slice(0, rangeValue.start)
const after = value.slice(rangeValue.end)
const needsLeadingSpace = before.length > 0 && !/\s$/.test(before)
const needsTrailingSpace = after.length > 0 && !/^\s/.test(after)
const leading = needsLeadingSpace ? `${before} ` : before
const trailingSpacer = needsTrailingSpace ? " " : ""
const nextValue = `${leading}${mentionText}${trailingSpacer}${after}`
const origin = options.getActiveContext()?.path === path ? "active" : "context"
const part: PromptAttachmentPart = {
kind: "attachment",
token: alias,
display: createAttachmentDisplay(path, node?.selection),
path,
selection: node?.selection,
origin,
}
options.setState("promptInput", nextValue)
if (input.value !== nextValue) {
input.value = nextValue
}
options.setState("inlineAliases", (prev) => {
const next = new Map(prev)
next.set(alias, part)
return next
})
options.addContextFile(path, node?.selection)
closeMention()
queueMicrotask(() => {
const caret = leading.length + mentionText.length + trailingSpacer.length
input.setSelectionRange(caret, caret)
syncMentionFromCaret(input)
})
}
return {
mentionResults,
mentionItems,
closeMention,
syncMentionFromCaret,
tryOpenMentionFromCaret,
updateMentionPosition,
handlePromptInput,
handleMentionKeyDown,
insertMention,
}
}
export function usePromptScrollSync(options: ScrollSyncOptions) {
let shouldAutoScroll = true
createEffect(() => {
options.state.promptInput
options.interim()
queueMicrotask(() => {
const input = options.getInputRef()
const overlay = options.getOverlayRef()
if (!input || !overlay) return
if (!shouldAutoScroll) {
overlay.scrollTop = input.scrollTop
if (options.state.mentionOpen) options.updateMentionPosition(input)
return
}
const maxInputScroll = input.scrollHeight - input.clientHeight
const next = maxInputScroll > 0 ? maxInputScroll : 0
input.scrollTop = next
overlay.scrollTop = next
if (options.state.mentionOpen) options.updateMentionPosition(input)
})
})
function handlePromptScroll(event: Event & { currentTarget: HTMLTextAreaElement }) {
const target = event.currentTarget
shouldAutoScroll = target.scrollTop + target.clientHeight >= target.scrollHeight - 4
const overlay = options.getOverlayRef()
if (overlay) overlay.scrollTop = target.scrollTop
if (options.state.mentionOpen) options.updateMentionPosition(target)
}
function resetScrollPosition() {
shouldAutoScroll = true
const input = options.getInputRef()
const overlay = options.getOverlayRef()
if (input) input.scrollTop = 0
if (overlay) overlay.scrollTop = 0
}
return {
handlePromptScroll,
resetScrollPosition,
setAutoScroll: (value: boolean) => {
shouldAutoScroll = value
},
}
}
+358 -72
View File
@@ -1,15 +1,25 @@
import { For, Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
import { For, Show, createMemo, onCleanup, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import { Popover } from "@kobalte/core/popover"
import { Button, FileIcon, Icon, IconButton, Tooltip } from "@/ui"
import { Select } from "@/components/select"
import { useLocal } from "@/context"
import type { FileContext, LocalFile } from "@/context/local"
import { getFilename } from "@/utils"
import { createSpeechRecognition } from "@/utils/speech"
import { getDirectory, getFilename } from "@/utils"
import { composeDisplaySegments, createAttachmentDisplay, parsePrompt, registerCandidate } from "./prompt-form-helpers"
import type {
AttachmentCandidate,
PromptAttachmentPart,
PromptAttachmentSegment,
PromptDisplaySegment,
PromptSubmitValue,
} from "./prompt-form-helpers"
import { useMentionController, usePromptScrollSync, usePromptSpeech, type PromptFormState } from "./prompt-form-hooks"
interface PromptFormProps {
class?: string
classList?: Record<string, boolean>
onSubmit: (prompt: string) => Promise<void> | void
onSubmit: (prompt: PromptSubmitValue) => Promise<void> | void
onOpenModelSelect: () => void
onInputRefChange?: (element: HTMLTextAreaElement | undefined) => void
}
@@ -17,8 +27,16 @@ interface PromptFormProps {
export default function PromptForm(props: PromptFormProps) {
const local = useLocal()
const [prompt, setPrompt] = createSignal("")
const [isDragOver, setIsDragOver] = createSignal(false)
const [state, setState] = createStore<PromptFormState>({
promptInput: "",
isDragOver: false,
mentionOpen: false,
mentionQuery: "",
mentionRange: undefined,
mentionIndex: 0,
mentionAnchorOffset: { x: 0, y: 0 },
inlineAliases: new Map<string, PromptAttachmentPart>(),
})
const placeholderText = "Start typing or speaking..."
@@ -28,79 +46,212 @@ export default function PromptForm(props: PromptFormProps) {
interim: interimTranscript,
start: startSpeech,
stop: stopSpeech,
} = createSpeechRecognition({
onFinal: (text) => setPrompt((prev) => (prev && !prev.endsWith(" ") ? prev + " " : prev) + text),
})
} = usePromptSpeech((updater) => setState("promptInput", updater))
let inputRef: HTMLTextAreaElement | undefined = undefined
let overlayContainerRef: HTMLDivElement | undefined = undefined
let shouldAutoScroll = true
let mentionMeasureRef: HTMLDivElement | undefined = undefined
const promptContent = createMemo(() => {
const base = prompt() || ""
const attachmentLookup = createMemo(() => {
const map = new Map<string, AttachmentCandidate>()
const activeFile = local.context.active()
if (activeFile) {
registerCandidate(
map,
{
origin: "active",
path: activeFile.path,
selection: activeFile.selection,
display: createAttachmentDisplay(activeFile.path, activeFile.selection),
},
[activeFile.path, getFilename(activeFile.path)],
)
}
for (const item of local.context.all()) {
registerCandidate(
map,
{
origin: "context",
path: item.path,
selection: item.selection,
display: createAttachmentDisplay(item.path, item.selection),
},
[item.path, getFilename(item.path)],
)
}
for (const [alias, part] of state.inlineAliases) {
registerCandidate(
map,
{
origin: part.origin,
path: part.path,
selection: part.selection,
display: part.display ?? createAttachmentDisplay(part.path, part.selection),
},
[alias],
)
}
return map
})
const parsedPrompt = createMemo(() => parsePrompt(state.promptInput, attachmentLookup()))
const baseParts = createMemo(() => parsedPrompt().parts)
const attachmentSegments = createMemo<PromptAttachmentSegment[]>(() =>
parsedPrompt().segments.filter((segment): segment is PromptAttachmentSegment => segment.kind === "attachment"),
)
const {
mentionResults,
mentionItems,
closeMention,
syncMentionFromCaret,
updateMentionPosition,
handlePromptInput,
handleMentionKeyDown,
insertMention,
} = useMentionController({
state,
setState,
attachmentSegments,
getInputRef: () => inputRef,
getOverlayRef: () => overlayContainerRef,
getMeasureRef: () => mentionMeasureRef,
searchFiles: (query) => local.file.search(query),
resolveFile: (path) => local.file.node(path) ?? undefined,
addContextFile: (path, selection) =>
local.context.add({
type: "file",
path,
selection,
}),
getActiveContext: () => local.context.active() ?? undefined,
})
const { handlePromptScroll, resetScrollPosition } = usePromptScrollSync({
state,
getInputRef: () => inputRef,
getOverlayRef: () => overlayContainerRef,
interim: () => (isRecording() ? interimTranscript() : ""),
updateMentionPosition,
})
const displaySegments = createMemo<PromptDisplaySegment[]>(() => {
const value = state.promptInput
const segments = parsedPrompt().segments
const interim = isRecording() ? interimTranscript() : ""
if (!base && !interim) {
return <span class="text-text-muted/70">{placeholderText}</span>
}
const needsSpace = base && interim && !base.endsWith(" ") && !interim.startsWith(" ")
return (
<>
<span class="text-text">{base}</span>
{interim && (
<span class="text-text-muted/60 italic">
{needsSpace ? " " : ""}
{interim}
</span>
)}
</>
)
return composeDisplaySegments(segments, value, interim)
})
createEffect(() => {
prompt()
interimTranscript()
queueMicrotask(() => {
if (!inputRef) return
if (!overlayContainerRef) return
if (!shouldAutoScroll) {
overlayContainerRef.scrollTop = inputRef.scrollTop
return
const hasDisplaySegments = createMemo(() => displaySegments().length > 0)
function handleAttachmentNavigation(
event: KeyboardEvent & { currentTarget: HTMLTextAreaElement },
direction: "left" | "right",
) {
const element = event.currentTarget
const caret = element.selectionStart ?? 0
const segments = attachmentSegments()
if (direction === "left") {
let match = segments.find((segment) => caret > segment.start && caret <= segment.end)
if (!match && element.selectionStart !== element.selectionEnd) {
match = segments.find(
(segment) => element.selectionStart === segment.start && element.selectionEnd === segment.end,
)
}
scrollPromptToEnd()
})
})
const handlePromptKeyDown = (event: KeyboardEvent & { currentTarget: HTMLTextAreaElement }) => {
if (event.isComposing) return
if (event.key === "Enter" && !event.shiftKey) {
if (!match) return false
event.preventDefault()
inputRef?.form?.requestSubmit()
if (element.selectionStart === match.start && element.selectionEnd === match.end) {
const next = Math.max(0, match.start)
element.setSelectionRange(next, next)
syncMentionFromCaret(element)
return true
}
element.setSelectionRange(match.start, match.end)
syncMentionFromCaret(element)
return true
}
if (direction === "right") {
let match = segments.find((segment) => caret >= segment.start && caret < segment.end)
if (!match && element.selectionStart !== element.selectionEnd) {
match = segments.find(
(segment) => element.selectionStart === segment.start && element.selectionEnd === segment.end,
)
}
if (!match) return false
event.preventDefault()
if (element.selectionStart === match.start && element.selectionEnd === match.end) {
const next = match.end
element.setSelectionRange(next, next)
syncMentionFromCaret(element)
return true
}
element.setSelectionRange(match.start, match.end)
syncMentionFromCaret(element)
return true
}
return false
}
const handlePromptScroll = (event: Event & { currentTarget: HTMLTextAreaElement }) => {
function renderAttachmentChip(part: PromptAttachmentPart, _placeholder: string) {
const display = part.display ?? createAttachmentDisplay(part.path, part.selection)
return <span class="truncate max-w-[16ch] text-primary">@{display}</span>
}
function renderTextSegment(value: string) {
if (!value) return undefined
return <span class="text-text">{value}</span>
}
function handlePromptKeyDown(event: KeyboardEvent & { currentTarget: HTMLTextAreaElement }) {
if (event.isComposing) return
const target = event.currentTarget
shouldAutoScroll = target.scrollTop + target.clientHeight >= target.scrollHeight - 4
if (overlayContainerRef) overlayContainerRef.scrollTop = target.scrollTop
}
const key = event.key
const scrollPromptToEnd = () => {
if (!inputRef) return
const maxInputScroll = inputRef.scrollHeight - inputRef.clientHeight
const next = maxInputScroll > 0 ? maxInputScroll : 0
inputRef.scrollTop = next
if (overlayContainerRef) overlayContainerRef.scrollTop = next
shouldAutoScroll = true
const handled = handleMentionKeyDown({
event,
mentionItems,
insertMention,
})
if (handled) return
if (!state.mentionOpen) {
if (key === "ArrowLeft") {
if (handleAttachmentNavigation(event, "left")) return
}
if (key === "ArrowRight") {
if (handleAttachmentNavigation(event, "right")) return
}
}
if (key === "ArrowLeft" || key === "ArrowRight" || key === "Home" || key === "End") {
queueMicrotask(() => {
syncMentionFromCaret(target)
})
}
if (key === "Enter" && !event.shiftKey) {
event.preventDefault()
target.form?.requestSubmit()
}
}
const handleSubmit = async (event: SubmitEvent) => {
event.preventDefault()
const currentPrompt = prompt()
setPrompt("")
shouldAutoScroll = true
if (overlayContainerRef) overlayContainerRef.scrollTop = 0
const parts = baseParts()
const text = parts
.map((part) => {
if (part.kind === "text") return part.value
return `@${part.path}`
})
.join("")
const currentPrompt: PromptSubmitValue = {
text,
parts,
}
setState("promptInput", "")
resetScrollPosition()
if (inputRef) {
inputRef.scrollTop = 0
inputRef.blur()
}
@@ -114,26 +265,25 @@ export default function PromptForm(props: PromptFormProps) {
return (
<form onSubmit={handleSubmit} class={props.class} classList={props.classList}>
<div
class="w-full max-w-xl min-w-0 p-2 mx-auto rounded-lg isolate backdrop-blur-xs
flex flex-col gap-1
bg-gradient-to-b from-background-panel/90 to-background/90
class="w-full min-w-0 p-2 mx-auto rounded-lg isolate backdrop-blur-xs
flex flex-col gap-1 bg-gradient-to-b from-background-panel/90 to-background/90
ring-1 ring-border-active/50 border border-transparent
focus-within:ring-2 focus-within:ring-primary/40 focus-within:border-primary
transition-all duration-200"
classList={{
"shadow-[0_0_33px_rgba(0,0,0,0.8)]": !!local.file.active(),
"ring-2 ring-primary/60 bg-primary/5": isDragOver(),
"ring-2 ring-primary/60 bg-primary/5": state.isDragOver,
}}
onDragEnter={(event) => {
const evt = event as unknown as globalThis.DragEvent
if (evt.dataTransfer?.types.includes("text/plain")) {
evt.preventDefault()
setIsDragOver(true)
setState("isDragOver", true)
}
}}
onDragLeave={(event) => {
if (event.currentTarget === event.target) {
setIsDragOver(false)
setState("isDragOver", false)
}
}}
onDragOver={(event) => {
@@ -146,7 +296,7 @@ export default function PromptForm(props: PromptFormProps) {
onDrop={(event) => {
const evt = event as unknown as globalThis.DragEvent
evt.preventDefault()
setIsDragOver(false)
setState("isDragOver", false)
const data = evt.dataTransfer?.getData("text/plain")
if (data && data.startsWith("file:")) {
@@ -177,9 +327,24 @@ export default function PromptForm(props: PromptFormProps) {
inputRef = element ?? undefined
props.onInputRefChange?.(inputRef)
}}
value={prompt()}
onInput={(event) => setPrompt(event.currentTarget.value)}
value={state.promptInput}
onInput={handlePromptInput}
onKeyDown={handlePromptKeyDown}
onClick={(event) =>
queueMicrotask(() => {
syncMentionFromCaret(event.currentTarget)
})
}
onSelect={(event) =>
queueMicrotask(() => {
syncMentionFromCaret(event.currentTarget)
})
}
onBlur={(event) => {
const next = event.relatedTarget as HTMLElement | null
if (next && next.closest('[data-mention-popover="true"]')) return
closeMention()
}}
onScroll={handlePromptScroll}
placeholder={placeholderText}
autocapitalize="off"
@@ -196,10 +361,30 @@ export default function PromptForm(props: PromptFormProps) {
}}
class="pointer-events-none absolute inset-0 overflow-hidden"
>
<div class="px-0.5 text-base font-light leading-relaxed whitespace-pre-wrap text-left text-text">
{promptContent()}
</div>
<PromptDisplayOverlay
hasDisplaySegments={hasDisplaySegments()}
displaySegments={displaySegments()}
placeholder={placeholderText}
renderAttachmentChip={renderAttachmentChip}
renderTextSegment={renderTextSegment}
/>
</div>
<div
ref={(element) => {
mentionMeasureRef = element ?? undefined
}}
class="pointer-events-none invisible absolute inset-0 whitespace-pre-wrap text-base font-light leading-relaxed px-0.5"
aria-hidden="true"
></div>
<MentionSuggestions
open={state.mentionOpen}
anchor={state.mentionAnchorOffset}
loading={mentionResults.loading}
items={mentionItems()}
activeIndex={state.mentionIndex}
onHover={(index) => setState("mentionIndex", index)}
onSelect={insertMention}
/>
</div>
<div class="flex justify-between items-center text-xs text-text-muted">
<div class="flex gap-2 items-center">
@@ -293,3 +478,104 @@ const FileTag = (props: { file: FileContext; onClose: () => void }) => (
</div>
</div>
)
function PromptDisplayOverlay(props: {
hasDisplaySegments: boolean
displaySegments: PromptDisplaySegment[]
placeholder: string
renderAttachmentChip: (part: PromptAttachmentPart, placeholder: string) => JSX.Element
renderTextSegment: (value: string) => JSX.Element | undefined
}) {
return (
<div class="px-0.5 text-base font-light leading-relaxed whitespace-pre-wrap text-left">
<Show when={props.hasDisplaySegments} fallback={<span class="text-text-muted/70">{props.placeholder}</span>}>
<For each={props.displaySegments}>
{(segment) => {
if (segment.kind === "text") {
return props.renderTextSegment(segment.value)
}
if (segment.kind === "attachment") {
return props.renderAttachmentChip(segment.part, segment.source)
}
return (
<span class="text-text-muted/60 italic">
{segment.leadingSpace ? ` ${segment.value}` : segment.value}
</span>
)
}}
</For>
</Show>
</div>
)
}
function MentionSuggestions(props: {
open: boolean
anchor: { x: number; y: number }
loading: boolean
items: string[]
activeIndex: number
onHover: (index: number) => void
onSelect: (path: string) => void
}) {
return (
<Popover open={props.open} modal={false} gutter={8} placement="bottom-start">
<Popover.Trigger class="hidden" />
<Popover.Anchor
class="pointer-events-none absolute top-0 left-0 w-0 h-0"
style={{ transform: `translate(${props.anchor.x}px, ${props.anchor.y}px)` }}
/>
<Popover.Portal>
<Popover.Content
data-mention-popover="true"
class="z-50 w-72 max-h-60 overflow-y-auto rounded-md border border-border-subtle/40 bg-background-panel shadow-[0_10px_30px_rgba(0,0,0,0.35)] focus:outline-none"
>
<div class="py-1">
<Show when={props.loading}>
<div class="flex items-center gap-2 px-3 py-2 text-xs text-text-muted">
<Icon name="refresh" size={12} class="animate-spin" />
<span>Searching</span>
</div>
</Show>
<Show when={!props.loading && props.items.length === 0}>
<div class="px-3 py-2 text-xs text-text-muted/80">No matching files</div>
</Show>
<For each={props.items}>
{(path, indexAccessor) => {
const index = indexAccessor()
const dir = getDirectory(path)
return (
<button
type="button"
onMouseDown={(event) => event.preventDefault()}
onMouseEnter={() => props.onHover(index)}
onClick={() => props.onSelect(path)}
class="w-full px-3 py-2 flex items-center gap-2 rounded-md text-left text-xs transition-colors"
classList={{
"bg-background-element text-text": index === props.activeIndex,
"text-text-muted": index !== props.activeIndex,
}}
>
<FileIcon node={{ path, type: "file" }} class="size-3 shrink-0" />
<div class="flex flex-col min-w-0">
<span class="truncate">{getFilename(path)}</span>
{dir && <span class="truncate text-text-muted/70">{dir}</span>}
</div>
</button>
)
}}
</For>
</div>
</Popover.Content>
</Popover.Portal>
</Popover>
)
}
export type {
PromptAttachmentPart,
PromptAttachmentSegment,
PromptContentPart,
PromptDisplaySegment,
PromptSubmitValue,
} from "./prompt-form-helpers"
+5 -4
View File
@@ -74,7 +74,7 @@ export function Select<T>(props: SelectProps<T> & ButtonProps) {
[props.class ?? ""]: !!props.class,
}}
>
<KobalteSelect.Value<T>>
<KobalteSelect.Value<T> class="truncate">
{(state) => {
const selected = state.selectedOption() ?? props.current
if (!selected) return props.placeholder || ""
@@ -84,10 +84,11 @@ export function Select<T>(props: SelectProps<T> & ButtonProps) {
</KobalteSelect.Value>
<KobalteSelect.Icon
classList={{
"size-fit shrink-0 text-text-muted transition-transform duration-100 data-[expanded]:rotate-180": true,
"group size-fit shrink-0 text-text-muted transition-transform duration-100": true,
}}
>
<Icon name="chevron-down" size={24} />
<Icon name="chevron-up" size={16} class="-my-2 group-data-[expanded]:rotate-180" />
<Icon name="chevron-down" size={16} class="-my-2 group-data-[expanded]:rotate-180" />
</KobalteSelect.Icon>
</KobalteSelect.Trigger>
<KobalteSelect.Portal>
@@ -99,7 +100,7 @@ export function Select<T>(props: SelectProps<T> & ButtonProps) {
"data-[expanded]:animate-in data-[expanded]:fade-in-0 data-[expanded]:zoom-in-95": true,
}}
>
<KobalteSelect.Listbox class="overflow-y-auto max-h-48" />
<KobalteSelect.Listbox class="overflow-y-auto max-h-48 whitespace-nowrap overflow-x-hidden" />
</KobalteSelect.Content>
</KobalteSelect.Portal>
</KobalteSelect>
@@ -1,26 +1,36 @@
import { useSync, useLocal } from "@/context"
import { Button, Tooltip } from "@/ui"
import { Tooltip } from "@/ui"
import { DateTime } from "luxon"
import { VList } from "virtua/solid"
export default function SessionList() {
const sync = useSync()
const local = useLocal()
return (
<VList data={sync.data.session} class="p-2">
<VList data={sync.data.session} class="p-3">
{(session) => (
<Tooltip placement="right" value={session.title} class="w-full min-w-0">
<Button
size="sm"
variant="ghost"
classList={{
"w-full min-w-0 py-1 text-left truncate justify-start text-text-muted text-xs": true,
"text-text!": local.session.active()?.id === session.id,
}}
<button
data-active={local.session.active()?.id === session.id}
class="group w-full min-w-0 text-left truncate justify-start text-xs my-0.5 cursor-pointer
flex flex-col gap-1 rounded-md p-2 border-[0.5px] border-transparent
hover:not-data-[active=true]:bg-background-panel
data-[active=true]:bg-background-element data-[active=true]:border-border-subtle"
onClick={() => local.session.setActive(session.id)}
>
<span class="truncate">{session.title}</span>
</Button>
<div class="flex gap-1 items-center">
<div
classList={{
"text-text/80 min-w-0 grow truncate": true,
"group-data-[active=true]:text-text!": true,
}}
>
{session.title}
</div>
<div class="shrink-0 text-text-muted/60">{DateTime.fromMillis(session.time.updated).toRelative()}</div>
</div>
<span class="text-text-muted truncate">{session.share?.url}</span>
</button>
</Tooltip>
)}
</VList>
+33 -1
View File
@@ -146,7 +146,10 @@ function init() {
}
for (const p of sync.data.changes) {
if (store.node[p.path] === undefined) {
fetch(p.path).then(() => setStore("node", p.path, "status", p))
fetch(p.path).then(() => {
if (store.node[p.path] === undefined) return
setStore("node", p.path, "status", p)
})
} else {
setStore("node", p.path, "status", p)
}
@@ -202,6 +205,13 @@ function init() {
}
}
const init = async (path: string) => {
const relativePath = relative(path)
if (!store.node[relativePath]) await fetch(path)
if (store.node[relativePath].loaded) return
return load(relativePath)
}
const open = async (path: string, options?: { pinned?: boolean; view?: LocalFile["view"] }) => {
const relativePath = relative(path)
if (!store.node[relativePath]) await fetch(path)
@@ -271,6 +281,7 @@ function init() {
update: (path: string, node: LocalFile) => setStore("node", path, reconcile(node)),
open,
load,
init,
close(path: string) {
setStore("opened", (opened) => opened.filter((x) => x !== path))
if (store.active === path) {
@@ -459,6 +470,11 @@ function init() {
return sync.session.get(store.active)
})
createEffect(() => {
if (!store.active) return
sync.session.sync(store.active)
})
return {
active,
setActive(sessionId: string | undefined) {
@@ -473,11 +489,16 @@ function init() {
const context = (() => {
const [store, setStore] = createStore<{
activeTab: boolean
files: string[]
activeFile?: string
items: (ContextItem & { key: string })[]
}>({
activeTab: true,
files: [],
items: [],
})
const files = createMemo(() => store.files.map((x) => file.node(x)))
const activeFile = createMemo(() => (store.activeFile ? file.node(store.activeFile) : undefined))
return {
all() {
@@ -505,6 +526,17 @@ function init() {
remove(key: string) {
setStore("items", (x) => x.filter((x) => x.key !== key))
},
files,
openFile(path: string) {
file.init(path).then(() => {
setStore("files", (x) => [...x, path])
setStore("activeFile", path)
})
},
activeFile,
setActiveFile(path: string | undefined) {
setStore("activeFile", path)
},
}
})()
+178 -104
View File
@@ -1,28 +1,30 @@
import { FileIcon, Icon, IconButton, Tooltip } from "@/ui"
import { Tabs } from "@/ui/tabs"
import * as KobalteTabs from "@kobalte/core/tabs"
import FileTree from "@/components/file-tree"
import EditorPane from "@/components/editor-pane"
import { For, Match, onCleanup, onMount, Show, Switch } from "solid-js"
import { SelectDialog } from "@/components/select-dialog"
import { useLocal } from "@/context"
import { ResizeableLayout, ResizeablePane } from "@/components/resizeable-pane"
import type { LocalFile } from "@/context/local"
import { useSync, useSDK, useLocal } from "@/context"
import type { LocalFile, TextSelection } from "@/context/local"
import SessionList from "@/components/session-list"
import SessionTimeline from "@/components/session-timeline"
import PromptForm, { type PromptContentPart, type PromptSubmitValue } from "@/components/prompt-form"
import { createStore } from "solid-js/store"
import { getDirectory, getFilename } from "@/utils"
import { Select } from "@/components/select"
import { Tabs } from "@/ui/tabs"
import { Code } from "@/components/code"
export default function Page() {
const local = useLocal()
const sync = useSync()
const sdk = useSDK()
const [store, setStore] = createStore({
clickTimer: undefined as number | undefined,
modelSelectOpen: false,
fileSelectOpen: false,
})
const layoutKey = "workspace"
const timelinePane = "timeline"
let inputRef: HTMLTextAreaElement | undefined = undefined
const MOD = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) ? "Meta" : "Control"
@@ -104,107 +106,178 @@ export default function Page() {
}
}
const handlePromptSubmit = async (prompt: PromptSubmitValue) => {
const existingSession = local.session.active()
let session = existingSession
if (!session) {
const created = await sdk.session.create()
session = created.data ?? undefined
}
if (!session) return
local.session.setActive(session.id)
interface SubmissionAttachment {
path: string
selection?: TextSelection
label: string
}
const createAttachmentKey = (path: string, selection?: TextSelection) => {
if (!selection) return path
return `${path}:${selection.startLine}:${selection.startChar}:${selection.endLine}:${selection.endChar}`
}
const formatAttachmentLabel = (path: string, selection?: TextSelection) => {
if (!selection) return getFilename(path)
return `${getFilename(path)} (${selection.startLine}-${selection.endLine})`
}
const toAbsolutePath = (path: string) => (path.startsWith("/") ? path : sync.absolute(path))
const attachments = new Map<string, SubmissionAttachment>()
const registerAttachment = (path: string, selection: TextSelection | undefined, label?: string) => {
if (!path) return
const key = createAttachmentKey(path, selection)
if (attachments.has(key)) return
attachments.set(key, {
path,
selection,
label: label ?? formatAttachmentLabel(path, selection),
})
}
const promptAttachments = prompt.parts.filter(
(part): part is Extract<PromptContentPart, { kind: "attachment" }> => part.kind === "attachment",
)
for (const part of promptAttachments) {
registerAttachment(part.path, part.selection, part.display)
}
const activeFile = local.context.active()
if (activeFile) {
registerAttachment(
activeFile.path,
activeFile.selection,
activeFile.name ?? formatAttachmentLabel(activeFile.path, activeFile.selection),
)
}
for (const contextFile of local.context.all()) {
registerAttachment(
contextFile.path,
contextFile.selection,
formatAttachmentLabel(contextFile.path, contextFile.selection),
)
}
const attachmentParts = Array.from(attachments.values()).map((attachment) => {
const absolute = toAbsolutePath(attachment.path)
const query = attachment.selection
? `?start=${attachment.selection.startLine}&end=${attachment.selection.endLine}`
: ""
return {
type: "file" as const,
mime: "text/plain",
url: `file://${absolute}${query}`,
filename: getFilename(attachment.path),
source: {
type: "file" as const,
text: {
value: `@${attachment.label}`,
start: 0,
end: 0,
},
path: absolute,
},
}
})
await sdk.session.prompt({
path: { id: session.id },
body: {
agent: local.agent.current()!.name,
model: {
modelID: local.model.current()!.id,
providerID: local.model.current()!.provider.id,
},
parts: [
{
type: "text",
text: prompt.text,
},
...attachmentParts,
],
},
})
}
const plus = (
<IconButton
class="text-text-muted/60 peer-data-[selected]/tab:opacity-100 peer-data-[selected]/tab:text-text peer-data-[selected]/tab:hover:bg-border-subtle hover:opacity-100 peer-hover/tab:opacity-100"
size="xs"
variant="secondary"
onClick={() => setStore("fileSelectOpen", true)}
>
<Icon name="plus" size={12} />
</IconButton>
)
return (
<div class="relative">
<ResizeableLayout
id={layoutKey}
defaults={{
explorer: { size: 24, visible: true },
editor: { size: 56, visible: true },
timeline: { size: 20, visible: false },
}}
class="h-screen"
>
<ResizeablePane
id="explorer"
minSize="150px"
maxSize="300px"
class="border-r border-border-subtle/30 bg-background z-10 overflow-hidden"
>
<Tabs class="relative flex flex-col h-full" defaultValue="files">
<div class="sticky top-0 shrink-0 flex">
<Tabs.List class="grow w-full after:hidden">
<Tabs.Trigger value="files" class="flex-1 justify-center text-xs">
Files
</Tabs.Trigger>
<Tabs.Trigger value="changes" class="flex-1 justify-center text-xs">
Changes
</Tabs.Trigger>
</Tabs.List>
</div>
<Tabs.Content value="files" class="grow min-h-0 py-2 bg-background">
<FileTree path="" onFileClick={handleFileClick} />
</Tabs.Content>
<Tabs.Content value="changes" class="grow min-h-0 py-2 bg-background">
<Show
when={local.file.changes().length}
fallback={<div class="px-2 text-xs text-text-muted">No changes</div>}
>
<ul class="">
<For each={local.file.changes()}>
{(path) => (
<li>
<button
onClick={() => local.file.open(path, { view: "diff-unified", pinned: true })}
class="w-full flex items-center px-2 py-0.5 gap-x-2 text-text-muted grow min-w-0 cursor-pointer hover:bg-background-element"
>
<FileIcon node={{ path, type: "file" }} class="shrink-0 size-3" />
<span class="text-xs text-text whitespace-nowrap">{getFilename(path)}</span>
<span class="text-xs text-text-muted/60 whitespace-nowrap truncate min-w-0">
{getDirectory(path)}
</span>
</button>
</li>
)}
</For>
</ul>
</Show>
</Tabs.Content>
</Tabs>
</ResizeablePane>
<ResizeablePane id="editor" minSize={30} maxSize={80} class="bg-background">
<EditorPane
layoutKey={layoutKey}
timelinePane={timelinePane}
onFileClick={handleFileClick}
<div class="relative h-screen flex flex-col">
<header class="h-12 shrink-0"></header>
<main class="h-[calc(100vh-3rem)] flex">
<div class="hidden shrink-0 w-64">
<SessionList />
</div>
<div class="grow w-full min-w-0 overflow-y-auto flex justify-center">
<Show when={local.session.active()}>
{(activeSession) => <SessionTimeline session={activeSession().id} class="max-w-xl" />}
</Show>
</div>
<div class="hidden shrink-0 w-56 p-2 h-full overflow-y-auto">
<FileTree path="" onFileClick={handleFileClick} />
</div>
<div class="hidden shrink-0 w-56 p-2">
<Show
when={local.file.changes().length}
fallback={<div class="px-2 text-xs text-text-muted">No changes</div>}
>
<ul class="">
<For each={local.file.changes()}>
{(path) => (
<li>
<button
onClick={() => local.file.open(path, { view: "diff-unified", pinned: true })}
class="w-full flex items-center px-2 py-0.5 gap-x-2 text-text-muted grow min-w-0 cursor-pointer hover:bg-background-element"
>
<FileIcon node={{ path, type: "file" }} class="shrink-0 size-3" />
<span class="text-xs text-text whitespace-nowrap">{getFilename(path)}</span>
<span class="text-xs text-text-muted/60 whitespace-nowrap truncate min-w-0">
{getDirectory(path)}
</span>
</button>
</li>
)}
</For>
</ul>
</Show>
</div>
<div class="hidden grow min-w-0">
<EditorPane onFileClick={handleFileClick} />
</div>
<div class="absolute bottom-4 inset-x-0 p-2 flex flex-col justify-center items-center gap-2 z-50">
<PromptForm
class="w-xl"
onSubmit={handlePromptSubmit}
onOpenModelSelect={() => setStore("modelSelectOpen", true)}
onInputRefChange={(element: HTMLTextAreaElement | null) => {
onInputRefChange={(element: HTMLTextAreaElement | undefined) => {
inputRef = element ?? undefined
}}
/>
</ResizeablePane>
<ResizeablePane
id="timeline"
minSize={20}
maxSize={40}
class="border-l border-border-subtle/30 bg-background z-10 overflow-hidden"
>
<div class="relative flex-1 min-h-0 overflow-y-auto overflow-x-hidden">
<Show when={local.session.active()} fallback={<SessionList />}>
{(activeSession) => (
<div class="relative">
<div class="sticky top-0 bg-background z-50 px-2 h-8 border-b border-border-subtle/30">
<div class="h-full flex items-center gap-2">
<IconButton
size="xs"
variant="ghost"
onClick={() => local.session.clearActive()}
class="text-text-muted hover:text-text"
>
<Icon name="arrow-left" size={14} />
</IconButton>
<h2 class="text-sm font-medium text-text truncate">
{activeSession().title || "Untitled Session"}
</h2>
</div>
</div>
<SessionTimeline session={activeSession().id} />
</div>
)}
</Show>
</div>
</ResizeablePane>
</ResizeableLayout>
</div>
</main>
<Show when={store.modelSelectOpen}>
<SelectDialog
key={(x) => `${x.provider.id}:${x.id}`}
@@ -270,7 +343,8 @@ export default function Page() {
</div>
)}
onClose={() => setStore("fileSelectOpen", false)}
onSelect={(x) => (x ? local.file.open(x, { pinned: true }) : undefined)}
onSelect={(x) => (x ? local.context.openFile(x) : undefined)}
// onSelect={(x) => (x ? local.file.open(x, { pinned: true }) : undefined)}
/>
</Show>
</div>
+4 -2
View File
@@ -1,6 +1,8 @@
export function getFilename(path: string) {
const parts = path.split("/")
return parts[parts.length - 1]
if (!path) return ""
const trimmed = path.replace(/[\/]+$/, "")
const parts = trimmed.split("/")
return parts[parts.length - 1] ?? ""
}
export function getDirectory(path: string) {
+2 -2
View File
@@ -1,11 +1,11 @@
{
"name": "@opencode-ai/function",
"version": "0.15.0",
"version": "0.15.5",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
"devDependencies": {
"@cloudflare/workers-types": "4.20250522.0",
"@cloudflare/workers-types": "catalog:",
"@types/node": "catalog:",
"typescript": "catalog:"
},
+13 -6
View File
@@ -1,11 +1,11 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "0.15.0",
"version": "0.15.5",
"name": "opencode",
"type": "module",
"private": true,
"scripts": {
"typecheck": "tsc --noEmit",
"typecheck": "tsgo --noEmit",
"test": "bun test",
"build": "./script/build.ts",
"dev": "bun run ./src/index.ts"
@@ -22,21 +22,28 @@
"@octokit/webhooks-types": "7.6.1",
"@parcel/watcher-win32-x64": "2.5.1",
"@standard-schema/spec": "1.0.0",
"@tsconfig/bun": "1.0.7",
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@types/turndown": "5.0.5",
"@types/yargs": "17.0.33",
"typescript": "catalog:",
"@typescript/native-preview": "catalog:",
"vscode-languageserver-types": "3.17.5",
"zod-to-json-schema": "3.24.5"
"zod-to-json-schema": "3.24.5",
"@opencode-ai/script": "workspace:*"
},
"dependencies": {
"@actions/core": "1.11.1",
"@actions/github": "6.0.1",
"@clack/prompts": "1.0.0-alpha.1",
"@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:",
"@modelcontextprotocol/sdk": "1.15.1",
"@openauthjs/openauth": "0.4.3",
"@octokit/graphql": "9.0.2",
"@octokit/rest": "22.0.0",
"@openauthjs/openauth": "catalog:",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@parcel/watcher": "2.5.1",
"@standard-schema/spec": "1.0.0",
@@ -44,7 +51,7 @@
"ai": "catalog:",
"chokidar": "4.0.3",
"decimal.js": "10.5.0",
"diff": "8.0.2",
"diff": "catalog:",
"fuzzysort": "3.1.0",
"gray-matter": "4.0.3",
"hono": "catalog:",
+7 -6
View File
@@ -5,6 +5,7 @@ process.chdir(dir)
import { $ } from "bun"
import pkg from "../package.json"
import { Script } from "@opencode-ai/script"
const GOARCH: Record<string, string> = {
arm64: "arm64",
@@ -25,12 +26,11 @@ const targets = [
await $`rm -rf dist`
const binaries: Record<string, string> = {}
const version = process.env["OPENCODE_VERSION"] ?? "dev"
for (const [os, arch] of targets) {
console.log(`building ${os}-${arch}`)
const name = `${pkg.name}-${os}-${arch}`
await $`mkdir -p dist/${name}/bin`
await $`CGO_ENABLED=0 GOOS=${os} GOARCH=${GOARCH[arch]} go build -ldflags="-s -w -X main.Version=${version}" -o ../opencode/dist/${name}/bin/tui ../tui/cmd/opencode/main.go`
await $`CGO_ENABLED=0 GOOS=${os} GOARCH=${GOARCH[arch]} go build -ldflags="-s -w -X main.Version=${Script.version}" -o ../opencode/dist/${name}/bin/tui ../tui/cmd/opencode/main.go`
.cwd("../tui")
.quiet()
@@ -43,12 +43,13 @@ for (const [os, arch] of targets) {
compile: {
target: `bun-${os}-${arch}` as any,
outfile: `dist/${name}/bin/opencode`,
execArgv: [`--user-agent=opencode/${version}`, `--env-file=""`, `--`],
execArgv: [`--user-agent=opencode/${Script.version}`, `--env-file=""`, `--`],
windows: {},
},
entrypoints: ["./src/index.ts"],
define: {
OPENCODE_VERSION: `'${version}'`,
OPENCODE_VERSION: `'${Script.version}'`,
OPENCODE_CHANNEL: `'${Script.channel}'`,
OPENCODE_TUI_PATH: `'../../../dist/${name}/bin/tui'`,
},
})
@@ -57,7 +58,7 @@ for (const [os, arch] of targets) {
JSON.stringify(
{
name,
version,
version: Script.version,
os: [os === "windows" ? "win32" : os],
cpu: [arch],
},
@@ -65,7 +66,7 @@ for (const [os, arch] of targets) {
2,
),
)
binaries[name] = version
binaries[name] = Script.version
}
export { binaries }
+20 -30
View File
@@ -1,20 +1,10 @@
#!/usr/bin/env bun
import { $ } from "bun"
import pkg from "../package.json"
import { Script } from "@opencode-ai/script"
const dir = new URL("..", import.meta.url).pathname
process.chdir(dir)
import { $ } from "bun"
import pkg from "../package.json"
const snapshot = process.env["OPENCODE_SNAPSHOT"] === "true"
let version = process.env["OPENCODE_VERSION"]
const tag = process.env["OPENCODE_TAG"] ?? (snapshot ? "snapshot" : "latest")
if (!version && snapshot) {
version = `0.0.0-${tag}-${new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "")}`
process.env["OPENCODE_VERSION"] = version
}
if (!version) throw new Error("OPENCODE_VERSION is required")
console.log(`publishing ${version}`)
const { binaries } = await import("./build.ts")
{
@@ -38,7 +28,7 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
preinstall: "node ./preinstall.mjs",
postinstall: "node ./postinstall.mjs",
},
version,
version: Script.version,
optionalDependencies: binaries,
},
null,
@@ -46,11 +36,11 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
),
)
for (const [name] of Object.entries(binaries)) {
await $`cd dist/${name} && chmod 777 -R . && bun publish --access public --tag ${tag}`
await $`cd dist/${name} && chmod 777 -R . && bun publish --access public --tag ${Script.channel}`
}
await $`cd ./dist/${pkg.name} && bun publish --access public --tag ${tag}`
await $`cd ./dist/${pkg.name} && bun publish --access public --tag ${Script.channel}`
if (!snapshot) {
if (!Script.preview) {
for (const key of Object.keys(binaries)) {
await $`cd dist/${key}/bin && zip -r ../../${key}.zip *`
}
@@ -67,7 +57,7 @@ if (!snapshot) {
"# Maintainer: adam",
"",
"pkgname='opencode-bin'",
`pkgver=${version.split("-")[0]}`,
`pkgver=${Script.version.split("-")[0]}`,
"options=('!debug' '!strip')",
"pkgrel=1",
"pkgdesc='The AI coding agent built for the terminal.'",
@@ -78,10 +68,10 @@ if (!snapshot) {
"conflicts=('opencode')",
"depends=('fzf' 'ripgrep')",
"",
`source_aarch64=("\${pkgname}_\${pkgver}_aarch64.zip::https://github.com/sst/opencode/releases/download/v${version}/opencode-linux-arm64.zip")`,
`source_aarch64=("\${pkgname}_\${pkgver}_aarch64.zip::https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-linux-arm64.zip")`,
`sha256sums_aarch64=('${arm64Sha}')`,
"",
`source_x86_64=("\${pkgname}_\${pkgver}_x86_64.zip::https://github.com/sst/opencode/releases/download/v${version}/opencode-linux-x64.zip")`,
`source_x86_64=("\${pkgname}_\${pkgver}_x86_64.zip::https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-linux-x64.zip")`,
`sha256sums_x86_64=('${x64Sha}')`,
"",
"package() {",
@@ -96,7 +86,7 @@ if (!snapshot) {
"# Maintainer: adam",
"",
"pkgname='opencode'",
`pkgver=${version.split("-")[0]}`,
`pkgver=${Script.version.split("-")[0]}`,
"options=('!debug' '!strip')",
"pkgrel=1",
"pkgdesc='The AI coding agent built for the terminal.'",
@@ -108,7 +98,7 @@ if (!snapshot) {
"depends=('fzf' 'ripgrep')",
"makedepends=('git' 'bun-bin' 'go')",
"",
`source=("opencode-\${pkgver}.tar.gz::https://github.com/sst/opencode/archive/v${version}.tar.gz")`,
`source=("opencode-\${pkgver}.tar.gz::https://github.com/sst/opencode/archive/v${Script.version}.tar.gz")`,
`sha256sums=('SKIP')`,
"",
"build() {",
@@ -139,7 +129,7 @@ if (!snapshot) {
await Bun.file(`./dist/aur-${pkg}/PKGBUILD`).write(pkgbuild)
await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO`
await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO`
await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${version}"`
await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${Script.version}"`
await $`cd ./dist/aur-${pkg} && git push`
break
} catch (e) {
@@ -157,11 +147,11 @@ if (!snapshot) {
"class Opencode < Formula",
` desc "The AI coding agent built for the terminal."`,
` homepage "https://github.com/sst/opencode"`,
` version "${version.split("-")[0]}"`,
` version "${Script.version.split("-")[0]}"`,
"",
" on_macos do",
" if Hardware::CPU.intel?",
` url "https://github.com/sst/opencode/releases/download/v${version}/opencode-darwin-x64.zip"`,
` url "https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-darwin-x64.zip"`,
` sha256 "${macX64Sha}"`,
"",
" def install",
@@ -169,7 +159,7 @@ if (!snapshot) {
" end",
" end",
" if Hardware::CPU.arm?",
` url "https://github.com/sst/opencode/releases/download/v${version}/opencode-darwin-arm64.zip"`,
` url "https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-darwin-arm64.zip"`,
` sha256 "${macArm64Sha}"`,
"",
" def install",
@@ -180,14 +170,14 @@ if (!snapshot) {
"",
" on_linux do",
" if Hardware::CPU.intel? and Hardware::CPU.is_64_bit?",
` url "https://github.com/sst/opencode/releases/download/v${version}/opencode-linux-x64.zip"`,
` url "https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-linux-x64.zip"`,
` sha256 "${x64Sha}"`,
" def install",
' bin.install "opencode"',
" end",
" end",
" if Hardware::CPU.arm? and Hardware::CPU.is_64_bit?",
` url "https://github.com/sst/opencode/releases/download/v${version}/opencode-linux-arm64.zip"`,
` url "https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-linux-arm64.zip"`,
` sha256 "${arm64Sha}"`,
" def install",
' bin.install "opencode"',
@@ -203,6 +193,6 @@ if (!snapshot) {
await $`git clone https://${process.env["GITHUB_TOKEN"]}@github.com/sst/homebrew-tap.git ./dist/homebrew-tap`
await Bun.file("./dist/homebrew-tap/opencode.rb").write(homebrewFormula)
await $`cd ./dist/homebrew-tap && git add opencode.rb`
await $`cd ./dist/homebrew-tap && git commit -m "Update to v${version}"`
await $`cd ./dist/homebrew-tap && git commit -m "Update to v${Script.version}"`
await $`cd ./dist/homebrew-tap && git push`
}
+13 -4
View File
@@ -10,10 +10,16 @@ export const AttachCommand = cmd({
command: "attach <server>",
describe: "attach to a running opencode server",
builder: (yargs) =>
yargs.positional("server", {
type: "string",
describe: "http://localhost:4096",
}),
yargs
.positional("server", {
type: "string",
describe: "http://localhost:4096",
})
.option("session", {
alias: ["s"],
describe: "session id to continue",
type: "string",
}),
handler: async (args) => {
let cmd = [] as string[]
const tui = Bun.embeddedFiles.find((item) => (item as File).name.includes("tui")) as File
@@ -36,6 +42,9 @@ export const AttachCommand = cmd({
await $`go build -o ${binaryName} ./main.go`.cwd(dir)
cmd = [path.join(dir, binaryName)]
}
if (args.session) {
cmd.push("--session", args.session)
}
Log.Default.info("tui", {
cmd,
})
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -156,8 +156,7 @@ export const TuiCommand = cmd({
})
;(async () => {
if (Installation.isDev()) return
if (Installation.isSnapshot()) return
// if (Installation.isLocal()) return
const config = await Config.global()
if (config.autoupdate === false || Flag.OPENCODE_DISABLE_AUTOUPDATE) return
const latest = await Installation.latest().catch(() => {})
@@ -214,7 +213,7 @@ function getOpencodeCommand(): string[] {
const execPath = process.execPath.toLowerCase()
if (Installation.isDev()) {
if (Installation.isLocal()) {
// In development, use bun to run the TypeScript entry point
return [execPath, "run", process.argv[1]]
}
+13 -3
View File
@@ -27,9 +27,19 @@ export const UpgradeCommand = {
const detectedMethod = await Installation.method()
const method = (args.method as Installation.Method) ?? detectedMethod
if (method === "unknown") {
prompts.log.error(`opencode is installed to ${process.execPath} and seems to be managed by a package manager`)
prompts.outro("Done")
return
prompts.log.error(`opencode is installed to ${process.execPath} and may be managed by a package manager`)
const install = await prompts.select({
message: "Install anyways?",
options: [
{ label: "Yes", value: true },
{ label: "No", value: false },
],
initialValue: false,
})
if (!install) {
prompts.outro("Done")
return
}
}
prompts.log.info("Using method: " + method)
const target = args.target ? args.target.replace(/^v/, "") : await Installation.latest()
+13 -4
View File
@@ -138,11 +138,20 @@ export namespace Config {
}
async function installDependencies(dir: string) {
if (Installation.isDev()) return
await Bun.write(path.join(dir, "package.json"), "{}")
await Bun.write(path.join(dir, ".gitignore"), ["node_modules", "package.json", "bun.lock", ".gitignore"].join("\n"))
if (Installation.isLocal()) return
const pkg = path.join(dir, "package.json")
if (!(await Bun.file(pkg).exists())) {
await Bun.write(pkg, "{}")
}
const gitignore = path.join(dir, ".gitignore")
const hasGitIgnore = await Bun.file(gitignore).exists()
if (!hasGitIgnore) await Bun.write(gitignore, ["node_modules", "package.json", "bun.lock", ".gitignore"].join("\n"))
await BunProc.run(
["add", "@opencode-ai/plugin@" + (Installation.isDev() ? "latest" : Installation.VERSION), "--exact"],
["add", "@opencode-ai/plugin@" + (Installation.isLocal() ? "latest" : Installation.VERSION), "--exact"],
{
cwd: dir,
},
+4 -1
View File
@@ -15,7 +15,10 @@ export namespace Format {
const enabled: Record<string, boolean> = {}
const cfg = await Config.get()
const formatters = { ...Formatter } as Record<string, Formatter.Info>
const formatters: Record<string, Formatter.Info> = {}
for (const item of Object.values(Formatter)) {
formatters[item.name] = item
}
for (const [name, item] of Object.entries(cfg.formatter ?? {})) {
if (item.disabled) {
delete formatters[name]
+2 -2
View File
@@ -51,10 +51,10 @@ const cli = yargs(hideBin(process.argv))
.middleware(async (opts) => {
await Log.init({
print: process.argv.includes("--print-logs"),
dev: Installation.isDev(),
dev: Installation.isLocal(),
level: (() => {
if (opts.logLevel) return opts.logLevel as Log.Level
if (Installation.isDev()) return "DEBUG"
if (Installation.isLocal()) return "DEBUG"
return "INFO"
})(),
})
+14 -14
View File
@@ -7,6 +7,7 @@ import { Log } from "../util/log"
declare global {
const OPENCODE_VERSION: string
const OPENCODE_CHANNEL: string
}
export namespace Installation {
@@ -40,16 +41,17 @@ export namespace Installation {
}
}
export function isSnapshot() {
return VERSION.startsWith("0.0.0")
export function isPreview() {
return CHANNEL !== "latest"
}
export function isDev() {
return VERSION === "dev"
export function isLocal() {
return CHANNEL === "local"
}
export async function method() {
if (process.execPath.includes(path.join(".opencode", "bin"))) return "curl"
if (process.execPath.includes(path.join(".local", "bin"))) return "curl"
const exec = process.execPath.toLowerCase()
const checks = [
@@ -135,18 +137,16 @@ export namespace Installation {
})
}
export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "dev"
export const USER_AGENT = `opencode/${VERSION}`
export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local"
export const CHANNEL = typeof OPENCODE_CHANNEL === "string" ? OPENCODE_CHANNEL : "local"
export const USER_AGENT = `opencode/${CHANNEL}/${VERSION}`
export async function latest() {
return fetch("https://api.github.com/repos/sst/opencode/releases/latest")
.then((res) => res.json())
.then((data) => {
if (typeof data.tag_name !== "string") {
log.error("GitHub API error", data)
throw new Error("failed to fetch latest version")
}
return data.tag_name.slice(1) as string
return fetch(`https://registry.npmjs.org/opencode-ai/${CHANNEL}`)
.then((res) => {
if (!res.ok) throw new Error(res.statusText)
return res.json()
})
.then((data: any) => data.version)
}
}
+2
View File
@@ -35,6 +35,7 @@ export namespace ModelsDev {
})
.optional(),
experimental: z.boolean().optional(),
status: z.enum(["alpha", "beta"]).optional(),
options: z.record(z.string(), z.any()),
provider: z.object({ npm: z.string() }).optional(),
})
@@ -76,6 +77,7 @@ export namespace ModelsDev {
headers: {
"User-Agent": Installation.USER_AGENT,
},
signal: AbortSignal.timeout(10 * 1000),
}).catch((e) => {
log.error("Failed to fetch models.dev", {
error: e,
+42 -20
View File
@@ -18,7 +18,7 @@ export namespace Provider {
type CustomLoader = (provider: ModelsDev.Provider) => Promise<{
autoload: boolean
getModel?: (sdk: any, modelID: string) => Promise<any>
getModel?: (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
options?: Record<string, any>
}>
@@ -58,7 +58,7 @@ export namespace Provider {
openai: async () => {
return {
autoload: false,
async getModel(sdk: any, modelID: string) {
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
return sdk.responses(modelID)
},
options: {},
@@ -67,8 +67,12 @@ export namespace Provider {
azure: async () => {
return {
autoload: false,
async getModel(sdk: any, modelID: string) {
return sdk.responses(modelID)
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
if (options?.["useCompletionUrls"]) {
return sdk.chat(modelID)
} else {
return sdk.responses(modelID)
}
},
options: {},
}
@@ -86,7 +90,7 @@ export namespace Provider {
region,
credentialProvider: fromNodeProviderChain(),
},
async getModel(sdk: any, modelID: string) {
async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
let regionPrefix = region.split("-")[0]
switch (regionPrefix) {
@@ -116,12 +120,17 @@ export namespace Provider {
break
}
case "ap": {
const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) =>
modelID.includes(m),
)
if (modelRequiresPrefix) {
regionPrefix = "apac"
modelID = `${regionPrefix}.${modelID}`
const isAustraliaRegion = ["ap-southeast-2", "ap-southeast-4"].includes(region)
if (isAustraliaRegion && modelID.startsWith("anthropic.claude-sonnet-4-5")) {
modelID = `au.${modelID}`
} else {
const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) =>
modelID.includes(m),
)
if (modelRequiresPrefix) {
regionPrefix = "apac"
modelID = `${regionPrefix}.${modelID}`
}
}
break
}
@@ -197,7 +206,7 @@ export namespace Provider {
[providerID: string]: {
source: Source
info: ModelsDev.Provider
getModel?: (sdk: any, modelID: string) => Promise<any>
getModel?: (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
options: Record<string, any>
}
} = {}
@@ -206,6 +215,8 @@ export namespace Provider {
{ providerID: string; modelID: string; info: ModelsDev.Model; language: LanguageModel; npm?: string }
>()
const sdk = new Map<number, SDK>()
// Maps `${provider}/${key}` to the providers actual model ID for custom aliases.
const realIdByKey = new Map<string, string>()
log.info("init")
@@ -213,7 +224,7 @@ export namespace Provider {
id: string,
options: Record<string, any>,
source: Source,
getModel?: (sdk: any, modelID: string) => Promise<any>,
getModel?: (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>,
) {
const provider = providers[id]
if (!provider) {
@@ -249,7 +260,7 @@ export namespace Provider {
for (const [modelID, model] of Object.entries(provider.models ?? {})) {
const existing = parsed.models[modelID]
const parsedModel: ModelsDev.Model = {
id: model.id ?? modelID,
id: modelID,
name: model.name ?? existing?.name ?? modelID,
release_date: model.release_date ?? existing?.release_date,
attachment: model.attachment ?? existing?.attachment ?? false,
@@ -286,6 +297,9 @@ export namespace Provider {
},
provider: model.provider ?? existing?.provider,
}
if (model.id && model.id !== modelID) {
realIdByKey.set(`${providerID}/${modelID}`, model.id)
}
parsed.models[modelID] = parsedModel
}
database[providerID] = parsed
@@ -347,7 +361,10 @@ export namespace Provider {
modelID !== "gpt-5-chat-latest" && !(providerID === "openrouter" && modelID === "openai/gpt-5-chat"),
)
// Filter out experimental models
.filter(([, model]) => !model.experimental || Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS),
.filter(
([, model]) =>
(!model.experimental && model.status !== "alpha") || Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS,
),
)
provider.info.models = filteredModels
@@ -362,6 +379,7 @@ export namespace Provider {
models,
providers,
sdk,
realIdByKey,
}
})
@@ -432,7 +450,11 @@ export namespace Provider {
const sdk = await getSDK(provider.info, info)
try {
const language = provider.getModel ? await provider.getModel(sdk, modelID) : sdk.languageModel(modelID)
const keyReal = `${providerID}/${modelID}`
const realID = s.realIdByKey.get(keyReal) ?? info.id
const language = provider.getModel
? await provider.getModel(sdk, realID, provider.options)
: sdk.languageModel(realID)
log.info("found", { providerID, modelID })
s.models.set(key, {
providerID,
@@ -512,11 +534,11 @@ export namespace Provider {
model_id: string
}[]
}
const models = state?.recently_used_models ?? []
if (models.length > 0) {
const [model] = state?.recently_used_models ?? []
if (model) {
return {
providerID: models[0].provider_id,
modelID: models[0].model_id,
providerID: model.provider_id,
modelID: model.model_id,
}
}
})
+62 -13
View File
@@ -25,6 +25,7 @@ import { Global } from "../global"
import { ProjectRoute } from "./project"
import { ToolRegistry } from "../tool/registry"
import { zodToJsonSchema } from "zod-to-json-schema"
import { SessionLock } from "../session/lock"
import { SessionPrompt } from "../session/prompt"
import { SessionCompaction } from "../session/compaction"
import { SessionRevert } from "../session/revert"
@@ -32,6 +33,8 @@ import { lazy } from "../util/lazy"
import { Todo } from "../session/todo"
import { InstanceBootstrap } from "../project/bootstrap"
import { MCP } from "../mcp"
import { Storage } from "../storage/storage"
import type { ContentfulStatusCode } from "hono/utils/http-status"
const ERRORS = {
400: {
@@ -41,17 +44,33 @@ const ERRORS = {
schema: resolver(
z
.object({
data: z.record(z.string(), z.any()),
data: z.any().nullable(),
errors: z.array(z.record(z.string(), z.any())),
success: z.literal(false),
})
.meta({
ref: "Error",
ref: "BadRequestError",
}),
),
},
},
},
404: {
description: "Not found",
content: {
"application/json": {
schema: resolver(
Storage.NotFoundError.Schema
)
},
},
},
} as const
function errors(...codes: number[]) {
return Object.fromEntries(codes.map((code) => [code, ERRORS[code as keyof typeof ERRORS]]))
}
export namespace Server {
const log = Log.create({ service: "server" })
@@ -67,12 +86,18 @@ export namespace Server {
error: err,
})
if (err instanceof NamedError) {
return c.json(err.toObject(), {
status: 400,
})
let status: ContentfulStatusCode
if (err instanceof Storage.NotFoundError)
status = 404
else if (err instanceof Provider.ModelNotFoundError)
status = 400
else
status = 500
return c.json(err.toObject(), { status })
}
return c.json(new NamedError.Unknown({ message: err.toString() }).toObject(), {
status: 400,
const message = err instanceof Error && err.stack ? err.stack : err.toString()
return c.json(new NamedError.Unknown({ message }).toObject(), {
status: 500,
})
})
.use(async (c, next) => {
@@ -151,7 +176,7 @@ export namespace Server {
},
},
},
...ERRORS,
...errors(400),
},
}),
validator("json", Config.Info),
@@ -175,7 +200,7 @@ export namespace Server {
},
},
},
...ERRORS,
...errors(400),
},
}),
async (c) => {
@@ -208,7 +233,7 @@ export namespace Server {
},
},
},
...ERRORS,
...errors(400),
},
}),
validator(
@@ -303,6 +328,7 @@ export namespace Server {
},
},
},
...errors(400, 404),
},
}),
validator(
@@ -331,6 +357,7 @@ export namespace Server {
},
},
},
...errors(400, 404),
},
}),
validator(
@@ -359,6 +386,7 @@ export namespace Server {
},
},
},
...errors(400, 404),
},
}),
validator(
@@ -379,7 +407,7 @@ export namespace Server {
description: "Create a new session",
operationId: "session.create",
responses: {
...ERRORS,
...errors(400),
200: {
description: "Successfully created session",
content: {
@@ -411,6 +439,7 @@ export namespace Server {
},
},
},
...errors(400, 404),
},
}),
validator(
@@ -438,6 +467,7 @@ export namespace Server {
},
},
},
...errors(400, 404),
},
}),
validator(
@@ -479,6 +509,7 @@ export namespace Server {
},
},
},
...errors(400, 404),
},
}),
validator(
@@ -539,6 +570,7 @@ export namespace Server {
},
},
},
...errors(400, 404),
},
}),
validator(
@@ -548,7 +580,7 @@ export namespace Server {
}),
),
async (c) => {
return c.json(SessionPrompt.abort(c.req.valid("param").id))
return c.json(SessionLock.abort(c.req.valid("param").id))
},
)
.post(
@@ -565,6 +597,7 @@ export namespace Server {
},
},
},
...errors(400, 404),
},
}),
validator(
@@ -594,6 +627,7 @@ export namespace Server {
},
},
},
...errors(400, 404),
},
}),
validator(
@@ -623,6 +657,7 @@ export namespace Server {
},
},
},
...errors(400, 404),
},
}),
validator(
@@ -659,6 +694,7 @@ export namespace Server {
},
},
},
...errors(400, 404),
},
}),
validator(
@@ -691,6 +727,7 @@ export namespace Server {
},
},
},
...errors(400, 404),
},
}),
validator(
@@ -725,6 +762,7 @@ export namespace Server {
},
},
},
...errors(400, 404),
},
}),
validator(
@@ -760,6 +798,7 @@ export namespace Server {
},
},
},
...errors(400, 404),
},
}),
validator(
@@ -790,6 +829,7 @@ export namespace Server {
},
},
},
...errors(400, 404),
},
}),
validator(
@@ -820,6 +860,7 @@ export namespace Server {
},
},
},
...errors(400, 404),
},
}),
validator(
@@ -850,6 +891,7 @@ export namespace Server {
},
},
},
...errors(400, 404),
},
}),
validator(
@@ -877,6 +919,7 @@ export namespace Server {
},
},
},
...errors(400, 404),
},
}),
validator(
@@ -1031,9 +1074,12 @@ export namespace Server {
}),
),
async (c) => {
/*
const query = c.req.valid("query").query
const result = await LSP.workspaceSymbol(query)
return c.json(result)
*/
return c.json([])
},
)
.get(
@@ -1127,6 +1173,7 @@ export namespace Server {
},
},
},
...errors(400),
},
}),
validator(
@@ -1218,6 +1265,7 @@ export namespace Server {
},
},
},
...errors(400),
},
}),
validator(
@@ -1350,6 +1398,7 @@ export namespace Server {
},
},
},
...errors(400),
},
}),
validator(
@@ -1401,7 +1450,7 @@ export namespace Server {
},
},
},
...ERRORS,
...errors(400),
},
}),
validator(
+46 -19
View File
@@ -1,4 +1,4 @@
import { generateText, type ModelMessage } from "ai"
import { streamText, type ModelMessage } from "ai"
import { Session } from "."
import { Identifier } from "../id/id"
import { Instance } from "../project/instance"
@@ -122,9 +122,24 @@ export namespace SessionCompaction {
created: Date.now(),
},
})) as MessageV2.Assistant
const generated = await generateText({
const part = (await Session.updatePart({
type: "text",
sessionID: input.sessionID,
messageID: msg.id,
id: Identifier.ascending("part"),
text: "",
time: {
start: Date.now(),
},
})) as MessageV2.TextPart
let summaryText = ""
const stream = streamText({
maxRetries: 10,
model: model.language,
providerOptions: {
[model.npm === "@ai-sdk/openai" ? "openai" : model.providerID]: model.info.options,
},
messages: [
...system.map(
(x): ModelMessage => ({
@@ -144,23 +159,35 @@ export namespace SessionCompaction {
},
],
})
const usage = Session.getUsage({ model: model.info, usage: generated.usage, metadata: generated.providerMetadata })
msg.cost += usage.cost
msg.tokens = usage.tokens
msg.summary = true
msg.time.completed = Date.now()
await Session.updateMessage(msg)
const part = await Session.updatePart({
type: "text",
sessionID: input.sessionID,
messageID: msg.id,
id: Identifier.ascending("part"),
text: generated.text,
time: {
start: Date.now(),
end: Date.now(),
},
})
for await (const value of stream.fullStream) {
switch (value.type) {
case "text-delta":
summaryText += value.text
await Session.updatePart({
...part,
text: summaryText,
})
break
case "text-end":
part.text = summaryText
await Session.updatePart({
...part,
})
break
case "finish": {
const usage = Session.getUsage({ model: model.info, usage: value.totalUsage, metadata: undefined })
msg.cost += usage.cost
msg.tokens = usage.tokens
msg.summary = true
msg.time.completed = Date.now()
await Session.updateMessage(msg)
part.time!.end = Date.now()
await Session.updatePart(part)
break
}
}
}
Bus.publish(Event.Compacted, {
sessionID: input.sessionID,
+17
View File
@@ -342,6 +342,23 @@ export namespace Session {
return part
})
export const removePart = fn(
z.object({
sessionID: Identifier.schema("session"),
messageID: Identifier.schema("message"),
partID: Identifier.schema("part"),
}),
async (input) => {
await Storage.remove(["part", input.messageID, input.partID])
Bus.publish(MessageV2.Event.PartRemoved, {
sessionID: input.sessionID,
messageID: input.messageID,
partID: input.partID,
})
return input.partID
},
)
export const getUsage = fn(
z.object({
model: z.custom<ModelsDev.Model>(),
+94
View File
@@ -0,0 +1,94 @@
import z from "zod/v4"
import { Instance } from "../project/instance"
import { Log } from "../util/log"
import { NamedError } from "../util/error"
export namespace SessionLock {
const log = Log.create({ service: "session.lock" })
export const LockedError = NamedError.create(
"SessionLockedError",
z.object({
sessionID: z.string(),
message: z.string(),
}),
)
type LockState = {
controller: AbortController
created: number
}
const state = Instance.state(
() => {
const locks = new Map<string, LockState>()
return {
locks,
}
},
async (current) => {
for (const [sessionID, lock] of current.locks) {
log.info("force abort", { sessionID })
lock.controller.abort()
}
current.locks.clear()
},
)
function get(sessionID: string) {
return state().locks.get(sessionID)
}
function unset(input: { sessionID: string; controller: AbortController }) {
const lock = get(input.sessionID)
if (!lock) return false
if (lock.controller !== input.controller) return false
state().locks.delete(input.sessionID)
return true
}
export function acquire(input: { sessionID: string }) {
const lock = get(input.sessionID)
if (lock) {
throw new LockedError({ sessionID: input.sessionID, message: `Session ${input.sessionID} is locked` })
}
const controller = new AbortController()
state().locks.set(input.sessionID, {
controller,
created: Date.now(),
})
log.info("locked", { sessionID: input.sessionID })
return {
signal: controller.signal,
abort() {
controller.abort()
unset({ sessionID: input.sessionID, controller })
},
async [Symbol.dispose]() {
const removed = unset({ sessionID: input.sessionID, controller })
if (removed) {
log.info("unlocked", { sessionID: input.sessionID })
}
},
}
}
export function abort(sessionID: string) {
const lock = get(sessionID)
if (!lock) return false
log.info("abort", { sessionID })
lock.controller.abort()
state().locks.delete(sessionID)
return true
}
export function isLocked(sessionID: string) {
return get(sessionID) !== undefined
}
export function assertUnlocked(sessionID: string) {
const lock = get(sessionID)
if (!lock) return
throw new LockedError({ sessionID, message: `Session ${sessionID} is locked` })
}
}
+60 -48
View File
@@ -22,6 +22,7 @@ import {
jsonSchema,
} from "ai"
import { SessionCompaction } from "./compaction"
import { SessionLock } from "./lock"
import { Instance } from "../project/instance"
import { Bus } from "../bus"
import { ProviderTransform } from "../provider/transform"
@@ -65,7 +66,6 @@ export namespace SessionPrompt {
const state = Instance.state(
() => {
const pending = new Map<string, AbortController>()
const queued = new Map<
string,
{
@@ -75,14 +75,11 @@ export namespace SessionPrompt {
>()
return {
pending,
queued,
}
},
async (state) => {
for (const [_, controller] of state.pending) {
controller.abort()
}
async (current) => {
current.queued.clear()
},
)
@@ -919,7 +916,7 @@ export namespace SessionPrompt {
const part = reasoningMap[value.id]
part.text += value.text
if (value.providerMetadata) part.metadata = value.providerMetadata
if (part.text) await Session.updatePart(part)
if (part.text.trim()) await Session.updatePart(part)
}
break
@@ -927,13 +924,14 @@ export namespace SessionPrompt {
if (value.id in reasoningMap) {
const part = reasoningMap[value.id]
part.text = part.text.trimEnd()
part.time = {
...part.time,
end: Date.now(),
if (part.text) {
part.time = {
...part.time,
end: Date.now(),
}
if (value.providerMetadata) part.metadata = value.providerMetadata
await Session.updatePart(part)
}
if (value.providerMetadata) part.metadata = value.providerMetadata
await Session.updatePart(part)
delete reasoningMap[value.id]
}
break
@@ -1088,19 +1086,21 @@ export namespace SessionPrompt {
if (currentText) {
currentText.text += value.text
if (value.providerMetadata) currentText.metadata = value.providerMetadata
if (currentText.text) await Session.updatePart(currentText)
if (currentText.text.trim()) await Session.updatePart(currentText)
}
break
case "text-end":
if (currentText) {
currentText.text = currentText.text.trimEnd()
currentText.time = {
start: Date.now(),
end: Date.now(),
if (currentText.text) {
currentText.time = {
start: Date.now(),
end: Date.now(),
}
if (value.providerMetadata) currentText.metadata = value.providerMetadata
await Session.updatePart(currentText)
}
if (value.providerMetadata) currentText.metadata = value.providerMetadata
await Session.updatePart(currentText)
}
currentText = undefined
break
@@ -1179,30 +1179,20 @@ export namespace SessionPrompt {
}
function isBusy(sessionID: string) {
return state().pending.has(sessionID)
}
export function abort(sessionID: string) {
const controller = state().pending.get(sessionID)
if (!controller) return false
log.info("aborting", {
sessionID,
})
controller.abort()
state().pending.delete(sessionID)
return true
return SessionLock.isLocked(sessionID)
}
function lock(sessionID: string) {
const handle = SessionLock.acquire({
sessionID,
})
log.info("locking", { sessionID })
if (state().pending.has(sessionID)) throw new Error("TODO")
const controller = new AbortController()
state().pending.set(sessionID, controller)
return {
signal: controller.signal,
signal: handle.signal,
abort: handle.abort,
async [Symbol.dispose]() {
handle[Symbol.dispose]()
log.info("unlocking", { sessionID })
state().pending.delete(sessionID)
const session = await Session.get(sessionID)
if (session.parentID) return
@@ -1290,20 +1280,42 @@ export namespace SessionPrompt {
const shell = process.env["SHELL"] ?? "bash"
const shellName = path.basename(shell)
const scripts: Record<string, string> = {
nu: input.command,
fish: `eval "${input.command}"`,
const invocations: Record<string, { args: string[] }> = {
nu: {
args: ["-c", input.command],
},
fish: {
args: ["-c", input.command],
},
zsh: {
args: [
"-c",
"-l",
`
[[ -f ~/.zshenv ]] && source ~/.zshenv >/dev/null 2>&1 || true
[[ -f "\${ZDOTDIR:-$HOME}/.zshrc" ]] && source "\${ZDOTDIR:-$HOME}/.zshrc" >/dev/null 2>&1 || true
${input.command}
`,
],
},
bash: {
args: [
"-c",
"-l",
`
[[ -f ~/.bashrc ]] && source ~/.bashrc >/dev/null 2>&1 || true
${input.command}
`,
],
},
// Fallback: any shell that doesn't match those above
"": {
args: ["-c", "-l", `${input.command}`],
},
}
const script =
scripts[shellName] ??
`[[ -f ~/.zshenv ]] && source ~/.zshenv >/dev/null 2>&1 || true
[[ -f "\${ZDOTDIR:-$HOME}/.zshrc" ]] && source "\${ZDOTDIR:-$HOME}/.zshrc" >/dev/null 2>&1 || true
[[ -f ~/.bashrc ]] && source ~/.bashrc >/dev/null 2>&1 || true
eval "${input.command}"`
const isFishOrNu = shellName === "fish" || shellName === "nu"
const args = isFishOrNu ? ["-c", script] : ["-c", "-l", script]
const matchingInvocation = invocations[shellName] ?? invocations[""]
const args = matchingInvocation?.args
const proc = spawn(shell, args, {
cwd: Instance.directory,
+10
View File
@@ -7,6 +7,7 @@ import { Log } from "../util/log"
import { splitWhen } from "remeda"
import { Storage } from "../storage/storage"
import { Bus } from "../bus"
import { SessionLock } from "./lock"
export namespace SessionRevert {
const log = Log.create({ service: "session.revert" })
@@ -19,6 +20,11 @@ export namespace SessionRevert {
export type RevertInput = z.infer<typeof RevertInput>
export async function revert(input: RevertInput) {
SessionLock.assertUnlocked(input.sessionID)
using _ = SessionLock.acquire({
sessionID: input.sessionID,
})
const all = await Session.messages(input.sessionID)
let lastUser: MessageV2.User | undefined
const session = await Session.get(input.sessionID)
@@ -64,6 +70,10 @@ export namespace SessionRevert {
export async function unrevert(input: { sessionID: string }) {
log.info("unreverting", input)
SessionLock.assertUnlocked(input.sessionID)
using _ = SessionLock.acquire({
sessionID: input.sessionID,
})
const session = await Session.get(input.sessionID)
if (!session.revert) return session
if (session.revert.snapshot) await Snapshot.restore(session.revert.snapshot)
+3 -1
View File
@@ -29,6 +29,8 @@ export namespace Todo {
}
export async function get(sessionID: string) {
return Storage.read<Info[]>(["todo", sessionID]) ?? []
return Storage.read<Info[]>(["todo", sessionID])
.then((x) => x || [])
.catch(() => [])
}
}
+1 -1
View File
@@ -67,7 +67,7 @@ export namespace Share {
export const URL =
process.env["OPENCODE_API"] ??
(Installation.isSnapshot() || Installation.isDev() ? "https://api.dev.opencode.ai" : "https://api.opencode.ai")
(Installation.isPreview() || Installation.isLocal() ? "https://api.dev.opencode.ai" : "https://api.opencode.ai")
export async function create(sessionID: string) {
return fetch(`${URL}/share_create`, {
+39 -10
View File
@@ -5,12 +5,21 @@ import { Global } from "../global"
import { lazy } from "../util/lazy"
import { Lock } from "../util/lock"
import { $ } from "bun"
import { NamedError } from "@/util/error"
import z from "zod"
export namespace Storage {
const log = Log.create({ service: "storage" })
type Migration = (dir: string) => Promise<void>
export const NotFoundError = NamedError.create(
"NotFoundError",
z.object({
message: z.string(),
}),
)
const MIGRATIONS: Migration[] = [
async (dir) => {
const project = path.resolve(dir, "../project")
@@ -131,31 +140,51 @@ export namespace Storage {
export async function remove(key: string[]) {
const dir = await state().then((x) => x.dir)
const target = path.join(dir, ...key) + ".json"
await fs.unlink(target).catch(() => {})
return withErrorHandling(async () => {
await fs.unlink(target).catch(() => {})
})
}
export async function read<T>(key: string[]) {
const dir = await state().then((x) => x.dir)
const target = path.join(dir, ...key) + ".json"
using _ = await Lock.read(target)
return Bun.file(target).json() as Promise<T>
return withErrorHandling(async () => {
using _ = await Lock.read(target)
return Bun.file(target).json() as Promise<T>
})
}
export async function update<T>(key: string[], fn: (draft: T) => void) {
const dir = await state().then((x) => x.dir)
const target = path.join(dir, ...key) + ".json"
using _ = await Lock.write("storage")
const content = await Bun.file(target).json()
fn(content)
await Bun.write(target, JSON.stringify(content, null, 2))
return content as T
return withErrorHandling(async () => {
using _ = await Lock.write("storage")
const content = await Bun.file(target).json()
fn(content)
await Bun.write(target, JSON.stringify(content, null, 2))
return content as T
})
}
export async function write<T>(key: string[], content: T) {
const dir = await state().then((x) => x.dir)
const target = path.join(dir, ...key) + ".json"
using _ = await Lock.write("storage")
await Bun.write(target, JSON.stringify(content, null, 2))
return withErrorHandling(async () => {
using _ = await Lock.write("storage")
await Bun.write(target, JSON.stringify(content, null, 2))
})
}
async function withErrorHandling<T>(body: () => Promise<T>) {
return body().catch((e) => {
if (!(e instanceof Error))
throw e
const errnoException = e as NodeJS.ErrnoException
if (errnoException.code === "ENOENT") {
throw new NotFoundError({ message: `Resource not found: ${errnoException.path}` })
}
throw e
})
}
const glob = new Bun.Glob("**/*")
+1 -1
View File
@@ -550,7 +550,7 @@ export const ContextAwareReplacer: Replacer = function* (content, find) {
}
}
function trimDiff(diff: string): string {
export function trimDiff(diff: string): string {
const lines = diff.split("\n")
const contentLines = lines.filter(
(line) =>
+1 -1
View File
@@ -1 +1 @@
Lists files and directories in a given path. The path parameter must be an absolute path, not a relative path. You can optionally provide an array of glob patterns to ignore with the ignore parameter. You should generally prefer the Glob and Grep tools, if you know which directories to search.
Lists files and directories in a given path. The path parameter must be absolute; omit it to use the current workspace directory. You can optionally provide an array of glob patterns to ignore with the ignore parameter. You should generally prefer the Glob and Grep tools, if you know which directories to search.
+2 -1
View File
@@ -6,6 +6,7 @@ import { Bus } from "../bus"
import { MessageV2 } from "../session/message-v2"
import { Identifier } from "../id/id"
import { Agent } from "../agent/agent"
import { SessionLock } from "../session/lock"
import { SessionPrompt } from "../session/prompt"
export const TaskTool = Tool.define("task", async () => {
@@ -53,7 +54,7 @@ export const TaskTool = Tool.define("task", async () => {
}
ctx.abort.addEventListener("abort", () => {
SessionPrompt.abort(session.id)
SessionLock.abort(session.id)
})
const result = await SessionPrompt.prompt({
messageID,
+12
View File
@@ -10,6 +10,8 @@ import { FileTime } from "../file/time"
import { Filesystem } from "../util/filesystem"
import { Instance } from "../project/instance"
import { Agent } from "../agent/agent"
import { createTwoFilesPatch } from "diff"
import { trimDiff } from "./edit"
export const WriteTool = Tool.define("write", {
description: DESCRIPTION,
@@ -27,6 +29,13 @@ export const WriteTool = Tool.define("write", {
const exists = await file.exists()
if (exists) await FileTime.assert(ctx.sessionID, filepath)
let oldContent = ""
let diff = ""
if (exists) {
oldContent = await file.text()
}
const agent = await Agent.get(ctx.agent)
if (agent.permission.edit === "ask")
await Permission.ask({
@@ -48,6 +57,9 @@ export const WriteTool = Tool.define("write", {
})
FileTime.read(ctx.sessionID, filepath)
// Generate diff for the write operation
diff = trimDiff(createTwoFilesPatch(filepath, filepath, oldContent, params.content))
let output = ""
await LSP.touchFile(filepath, true)
const diagnostics = await LSP.diagnostics()
+1 -1
View File
@@ -2,7 +2,7 @@ import { AsyncLocalStorage } from "async_hooks"
export namespace Context {
export class NotFound extends Error {
constructor(public readonly name: string) {
constructor(public override readonly name: string) {
super(`No context found for ${name}`)
}
}
+1 -4
View File
@@ -1,7 +1,4 @@
import z from "zod/v4"
// import { Log } from "./log"
// const log = Log.create()
export abstract class NamedError extends Error {
abstract schema(): z.core.$ZodType
@@ -19,7 +16,7 @@ export abstract class NamedError extends Error {
const result = class extends NamedError {
public static readonly Schema = schema
public readonly name = name as Name
public override readonly name = name as Name
constructor(
public readonly data: z.input<Data>,
@@ -14,7 +14,7 @@ async function bootstrap() {
await Bun.write(`${dir}/a.txt`, aContent)
await Bun.write(`${dir}/b.txt`, bContent)
await $`git add .`.cwd(dir).quiet()
await $`git commit -m init`.cwd(dir).quiet()
await $`git commit --no-gpg-sign -m init`.cwd(dir).quiet()
return {
aContent,
bContent,
+1 -1
View File
@@ -6,8 +6,8 @@
"jsxImportSource": "@opentui/solid",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"types": [],
"noUncheckedIndexedAccess": false,
"customConditions": ["browser"],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@tui/*": ["./src/cli/cmd/tui/*"]
+5 -4
View File
@@ -1,10 +1,10 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "0.15.0",
"version": "0.15.5",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"typecheck": "tsgo --noEmit",
"build": "tsc"
},
"exports": {
@@ -20,7 +20,8 @@
},
"devDependencies": {
"@tsconfig/node22": "catalog:",
"@types/node": "24.7.1",
"typescript": "catalog:"
"@types/node": "catalog:",
"typescript": "catalog:",
"@typescript/native-preview": "catalog:"
}
}
+4 -12
View File
@@ -1,29 +1,21 @@
#!/usr/bin/env bun
import { Script } from "@opencode-ai/script"
import { $ } from "bun"
const dir = new URL("..", import.meta.url).pathname
process.chdir(dir)
import { $ } from "bun"
await $`bun tsc`
const pkg = await import("../package.json").then((m) => m.default)
const original = JSON.parse(JSON.stringify(pkg))
for (const [key, value] of Object.entries(pkg.exports)) {
const file = value.replace("./src/", "./dist/").replace(".ts", "")
// @ts-ignore
pkg.exports[key] = {
import: file + ".js",
types: file + ".d.ts",
}
}
await Bun.write("package.json", JSON.stringify(pkg, null, 2))
const snapshot = process.env["OPENCODE_SNAPSHOT"] === "true"
if (snapshot) {
await $`bun publish --tag snapshot --access public`
}
if (!snapshot) {
await $`bun publish --access public`
}
await $`bun publish --tag ${Script.channel} --access public`
await Bun.write("package.json", JSON.stringify(original, null, 2))
+10
View File
@@ -0,0 +1,10 @@
{
"$schema": "https://json.schemastore.org/package",
"name": "@opencode-ai/script",
"devDependencies": {
"@types/bun": "catalog:"
},
"exports": {
".": "./src/index.ts"
}
}
+35
View File
@@ -0,0 +1,35 @@
import { $ } from "bun"
if (process.versions.bun !== "1.3.0") {
throw new Error("This script requires bun@1.3.0")
}
const CHANNEL = process.env["OPENCODE_CHANNEL"] ?? (await $`git branch --show-current`.text().then((x) => x.trim()))
const IS_PREVIEW = CHANNEL !== "latest"
const VERSION = await (async () => {
if (IS_PREVIEW) return `0.0.0-${CHANNEL}-${new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "")}`
const version = await fetch("https://registry.npmjs.org/opencode-ai/latest")
.then((res) => {
if (!res.ok) throw new Error(res.statusText)
return res.json()
})
.then((data: any) => data.version)
const [major, minor, patch] = version.split(".").map((x: string) => Number(x) || 0)
const t = process.env["OPENCODE_BUMP"]?.toLowerCase()
if (t === "major") return `${major + 1}.0.0`
if (t === "minor") return `${major}.${minor + 1}.0`
return `${major}.${minor}.${patch + 1}`
})()
export const Script = {
get channel() {
return CHANNEL
},
get version() {
return VERSION
},
get preview() {
return IS_PREVIEW
},
}
console.log(`opencode script`, JSON.stringify(Script, null, 2))
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/bun/tsconfig.json",
"compilerOptions": {
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"noUncheckedIndexedAccess": false
}
}
+5 -4
View File
@@ -1,10 +1,10 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "0.15.0",
"version": "0.15.5",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"typecheck": "tsgo --noEmit",
"build": "./script/build.ts"
},
"exports": {
@@ -18,8 +18,9 @@
"devDependencies": {
"@hey-api/openapi-ts": "0.81.0",
"@tsconfig/node22": "catalog:",
"@types/node": "24.7.1",
"typescript": "catalog:"
"@types/node": "catalog:",
"typescript": "catalog:",
"@typescript/native-preview": "catalog:"
},
"dependencies": {},
"publishConfig": {
+4 -11
View File
@@ -1,10 +1,11 @@
#!/usr/bin/env bun
import { Script } from "@opencode-ai/script"
import { $ } from "bun"
const dir = new URL("..", import.meta.url).pathname
process.chdir(dir)
import { $ } from "bun"
await import("./build")
const pkg = await import("../package.json").then((m) => m.default)
@@ -18,13 +19,5 @@ for (const [key, value] of Object.entries(pkg.exports)) {
}
}
await Bun.write("package.json", JSON.stringify(pkg, null, 2))
const snapshot = process.env["OPENCODE_SNAPSHOT"] === "true"
if (snapshot) {
await $`bun publish --tag snapshot --access public`
}
if (!snapshot) {
await $`bun publish --access public`
}
await $`bun publish --tag ${Script.channel} --access public`
await Bun.write("package.json", JSON.stringify(original, null, 2))
+46 -21
View File
@@ -26,42 +26,60 @@ import type {
SessionCreateErrors,
SessionDeleteData,
SessionDeleteResponses,
SessionDeleteErrors,
SessionGetData,
SessionGetResponses,
SessionGetErrors,
SessionUpdateData,
SessionUpdateResponses,
SessionUpdateErrors,
SessionChildrenData,
SessionChildrenResponses,
SessionChildrenErrors,
SessionTodoData,
SessionTodoResponses,
SessionTodoErrors,
SessionInitData,
SessionInitResponses,
SessionInitErrors,
SessionForkData,
SessionForkResponses,
SessionAbortData,
SessionAbortResponses,
SessionAbortErrors,
SessionUnshareData,
SessionUnshareResponses,
SessionUnshareErrors,
SessionShareData,
SessionShareResponses,
SessionShareErrors,
SessionSummarizeData,
SessionSummarizeResponses,
SessionSummarizeErrors,
SessionMessagesData,
SessionMessagesResponses,
SessionMessagesErrors,
SessionPromptData,
SessionPromptResponses,
SessionPromptErrors,
SessionMessageData,
SessionMessageResponses,
SessionMessageErrors,
SessionCommandData,
SessionCommandResponses,
SessionCommandErrors,
SessionShellData,
SessionShellResponses,
SessionShellErrors,
SessionRevertData,
SessionRevertResponses,
SessionRevertErrors,
SessionUnrevertData,
SessionUnrevertResponses,
SessionUnrevertErrors,
PostSessionIdPermissionsPermissionIdData,
PostSessionIdPermissionsPermissionIdResponses,
PostSessionIdPermissionsPermissionIdErrors,
CommandListData,
CommandListResponses,
ConfigProvidersData,
@@ -80,12 +98,14 @@ import type {
FileStatusResponses,
AppLogData,
AppLogResponses,
AppLogErrors,
AppAgentsData,
AppAgentsResponses,
McpStatusData,
McpStatusResponses,
TuiAppendPromptData,
TuiAppendPromptResponses,
TuiAppendPromptErrors,
TuiOpenHelpData,
TuiOpenHelpResponses,
TuiOpenSessionsData,
@@ -100,6 +120,7 @@ import type {
TuiClearPromptResponses,
TuiExecuteCommandData,
TuiExecuteCommandResponses,
TuiExecuteCommandErrors,
TuiShowToastData,
TuiShowToastResponses,
AuthSetData,
@@ -258,7 +279,7 @@ class Session extends _HeyApiClient {
* Delete a session and all its data
*/
public delete<ThrowOnError extends boolean = false>(options: Options<SessionDeleteData, ThrowOnError>) {
return (options.client ?? this._client).delete<SessionDeleteResponses, unknown, ThrowOnError>({
return (options.client ?? this._client).delete<SessionDeleteResponses, SessionDeleteErrors, ThrowOnError>({
url: "/session/{id}",
...options,
})
@@ -268,7 +289,7 @@ class Session extends _HeyApiClient {
* Get session
*/
public get<ThrowOnError extends boolean = false>(options: Options<SessionGetData, ThrowOnError>) {
return (options.client ?? this._client).get<SessionGetResponses, unknown, ThrowOnError>({
return (options.client ?? this._client).get<SessionGetResponses, SessionGetErrors, ThrowOnError>({
url: "/session/{id}",
...options,
})
@@ -278,7 +299,7 @@ class Session extends _HeyApiClient {
* Update session properties
*/
public update<ThrowOnError extends boolean = false>(options: Options<SessionUpdateData, ThrowOnError>) {
return (options.client ?? this._client).patch<SessionUpdateResponses, unknown, ThrowOnError>({
return (options.client ?? this._client).patch<SessionUpdateResponses, SessionUpdateErrors, ThrowOnError>({
url: "/session/{id}",
...options,
headers: {
@@ -292,7 +313,7 @@ class Session extends _HeyApiClient {
* Get a session's children
*/
public children<ThrowOnError extends boolean = false>(options: Options<SessionChildrenData, ThrowOnError>) {
return (options.client ?? this._client).get<SessionChildrenResponses, unknown, ThrowOnError>({
return (options.client ?? this._client).get<SessionChildrenResponses, SessionChildrenErrors, ThrowOnError>({
url: "/session/{id}/children",
...options,
})
@@ -302,7 +323,7 @@ class Session extends _HeyApiClient {
* Get the todo list for a session
*/
public todo<ThrowOnError extends boolean = false>(options: Options<SessionTodoData, ThrowOnError>) {
return (options.client ?? this._client).get<SessionTodoResponses, unknown, ThrowOnError>({
return (options.client ?? this._client).get<SessionTodoResponses, SessionTodoErrors, ThrowOnError>({
url: "/session/{id}/todo",
...options,
})
@@ -312,7 +333,7 @@ class Session extends _HeyApiClient {
* Analyze the app and create an AGENTS.md file
*/
public init<ThrowOnError extends boolean = false>(options: Options<SessionInitData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionInitResponses, unknown, ThrowOnError>({
return (options.client ?? this._client).post<SessionInitResponses, SessionInitErrors, ThrowOnError>({
url: "/session/{id}/init",
...options,
headers: {
@@ -340,7 +361,7 @@ class Session extends _HeyApiClient {
* Abort a session
*/
public abort<ThrowOnError extends boolean = false>(options: Options<SessionAbortData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionAbortResponses, unknown, ThrowOnError>({
return (options.client ?? this._client).post<SessionAbortResponses, SessionAbortErrors, ThrowOnError>({
url: "/session/{id}/abort",
...options,
})
@@ -350,7 +371,7 @@ class Session extends _HeyApiClient {
* Unshare the session
*/
public unshare<ThrowOnError extends boolean = false>(options: Options<SessionUnshareData, ThrowOnError>) {
return (options.client ?? this._client).delete<SessionUnshareResponses, unknown, ThrowOnError>({
return (options.client ?? this._client).delete<SessionUnshareResponses, SessionUnshareErrors, ThrowOnError>({
url: "/session/{id}/share",
...options,
})
@@ -360,7 +381,7 @@ class Session extends _HeyApiClient {
* Share a session
*/
public share<ThrowOnError extends boolean = false>(options: Options<SessionShareData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionShareResponses, unknown, ThrowOnError>({
return (options.client ?? this._client).post<SessionShareResponses, SessionShareErrors, ThrowOnError>({
url: "/session/{id}/share",
...options,
})
@@ -370,7 +391,7 @@ class Session extends _HeyApiClient {
* Summarize the session
*/
public summarize<ThrowOnError extends boolean = false>(options: Options<SessionSummarizeData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionSummarizeResponses, unknown, ThrowOnError>({
return (options.client ?? this._client).post<SessionSummarizeResponses, SessionSummarizeErrors, ThrowOnError>({
url: "/session/{id}/summarize",
...options,
headers: {
@@ -384,7 +405,7 @@ class Session extends _HeyApiClient {
* List messages for a session
*/
public messages<ThrowOnError extends boolean = false>(options: Options<SessionMessagesData, ThrowOnError>) {
return (options.client ?? this._client).get<SessionMessagesResponses, unknown, ThrowOnError>({
return (options.client ?? this._client).get<SessionMessagesResponses, SessionMessagesErrors, ThrowOnError>({
url: "/session/{id}/message",
...options,
})
@@ -394,7 +415,7 @@ class Session extends _HeyApiClient {
* Create and send a new message to a session
*/
public prompt<ThrowOnError extends boolean = false>(options: Options<SessionPromptData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionPromptResponses, unknown, ThrowOnError>({
return (options.client ?? this._client).post<SessionPromptResponses, SessionPromptErrors, ThrowOnError>({
url: "/session/{id}/message",
...options,
headers: {
@@ -408,7 +429,7 @@ class Session extends _HeyApiClient {
* Get a message from a session
*/
public message<ThrowOnError extends boolean = false>(options: Options<SessionMessageData, ThrowOnError>) {
return (options.client ?? this._client).get<SessionMessageResponses, unknown, ThrowOnError>({
return (options.client ?? this._client).get<SessionMessageResponses, SessionMessageErrors, ThrowOnError>({
url: "/session/{id}/message/{messageID}",
...options,
})
@@ -418,7 +439,7 @@ class Session extends _HeyApiClient {
* Send a new command to a session
*/
public command<ThrowOnError extends boolean = false>(options: Options<SessionCommandData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionCommandResponses, unknown, ThrowOnError>({
return (options.client ?? this._client).post<SessionCommandResponses, SessionCommandErrors, ThrowOnError>({
url: "/session/{id}/command",
...options,
headers: {
@@ -432,7 +453,7 @@ class Session extends _HeyApiClient {
* Run a shell command
*/
public shell<ThrowOnError extends boolean = false>(options: Options<SessionShellData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionShellResponses, unknown, ThrowOnError>({
return (options.client ?? this._client).post<SessionShellResponses, SessionShellErrors, ThrowOnError>({
url: "/session/{id}/shell",
...options,
headers: {
@@ -446,7 +467,7 @@ class Session extends _HeyApiClient {
* Revert a message
*/
public revert<ThrowOnError extends boolean = false>(options: Options<SessionRevertData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionRevertResponses, unknown, ThrowOnError>({
return (options.client ?? this._client).post<SessionRevertResponses, SessionRevertErrors, ThrowOnError>({
url: "/session/{id}/revert",
...options,
headers: {
@@ -460,7 +481,7 @@ class Session extends _HeyApiClient {
* Restore all reverted messages
*/
public unrevert<ThrowOnError extends boolean = false>(options: Options<SessionUnrevertData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionUnrevertResponses, unknown, ThrowOnError>({
return (options.client ?? this._client).post<SessionUnrevertResponses, SessionUnrevertErrors, ThrowOnError>({
url: "/session/{id}/unrevert",
...options,
})
@@ -548,7 +569,7 @@ class App extends _HeyApiClient {
* Write a log entry to the server logs
*/
public log<ThrowOnError extends boolean = false>(options?: Options<AppLogData, ThrowOnError>) {
return (options?.client ?? this._client).post<AppLogResponses, unknown, ThrowOnError>({
return (options?.client ?? this._client).post<AppLogResponses, AppLogErrors, ThrowOnError>({
url: "/log",
...options,
headers: {
@@ -586,7 +607,7 @@ class Tui extends _HeyApiClient {
* Append prompt to the TUI
*/
public appendPrompt<ThrowOnError extends boolean = false>(options?: Options<TuiAppendPromptData, ThrowOnError>) {
return (options?.client ?? this._client).post<TuiAppendPromptResponses, unknown, ThrowOnError>({
return (options?.client ?? this._client).post<TuiAppendPromptResponses, TuiAppendPromptErrors, ThrowOnError>({
url: "/tui/append-prompt",
...options,
headers: {
@@ -660,7 +681,7 @@ class Tui extends _HeyApiClient {
* Execute a TUI command (e.g. agent_cycle)
*/
public executeCommand<ThrowOnError extends boolean = false>(options?: Options<TuiExecuteCommandData, ThrowOnError>) {
return (options?.client ?? this._client).post<TuiExecuteCommandResponses, unknown, ThrowOnError>({
return (options?.client ?? this._client).post<TuiExecuteCommandResponses, TuiExecuteCommandErrors, ThrowOnError>({
url: "/tui/execute-command",
...options,
headers: {
@@ -720,7 +741,11 @@ export class OpencodeClient extends _HeyApiClient {
public postSessionIdPermissionsPermissionId<ThrowOnError extends boolean = false>(
options: Options<PostSessionIdPermissionsPermissionIdData, ThrowOnError>,
) {
return (options.client ?? this._client).post<PostSessionIdPermissionsPermissionIdResponses, unknown, ThrowOnError>({
return (options.client ?? this._client).post<
PostSessionIdPermissionsPermissionIdResponses,
PostSessionIdPermissionsPermissionIdErrors,
ThrowOnError
>({
url: "/session/{id}/permissions/{permissionID}",
...options,
headers: {
+281 -8
View File
@@ -418,6 +418,7 @@ export type Config = {
output: Array<"text" | "audio" | "image" | "video" | "pdf">
}
experimental?: boolean
status?: "alpha" | "beta"
options?: {
[key: string]: unknown
}
@@ -508,10 +509,12 @@ export type Config = {
}
}
export type _Error = {
data: {
export type BadRequestError = {
data: unknown | null
errors: Array<{
[key: string]: unknown
}
}>
success: false
}
export type ToolIds = Array<string>
@@ -554,6 +557,13 @@ export type Session = {
}
}
export type NotFoundError = {
name: "NotFoundError"
data: {
message: string
}
}
export type Todo = {
/**
* Brief description of the task
@@ -914,6 +924,7 @@ export type Model = {
output: Array<"text" | "audio" | "image" | "video" | "pdf">
}
experimental?: boolean
status?: "alpha" | "beta"
options: {
[key: string]: unknown
}
@@ -1259,7 +1270,7 @@ export type ConfigUpdateErrors = {
/**
* Bad request
*/
400: _Error
400: BadRequestError
}
export type ConfigUpdateError = ConfigUpdateErrors[keyof ConfigUpdateErrors]
@@ -1286,7 +1297,7 @@ export type ToolIdsErrors = {
/**
* Bad request
*/
400: _Error
400: BadRequestError
}
export type ToolIdsError = ToolIdsErrors[keyof ToolIdsErrors]
@@ -1315,7 +1326,7 @@ export type ToolListErrors = {
/**
* Bad request
*/
400: _Error
400: BadRequestError
}
export type ToolListError = ToolListErrors[keyof ToolListErrors]
@@ -1381,7 +1392,7 @@ export type SessionCreateErrors = {
/**
* Bad request
*/
400: _Error
400: BadRequestError
}
export type SessionCreateError = SessionCreateErrors[keyof SessionCreateErrors]
@@ -1406,6 +1417,19 @@ export type SessionDeleteData = {
url: "/session/{id}"
}
export type SessionDeleteErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type SessionDeleteError = SessionDeleteErrors[keyof SessionDeleteErrors]
export type SessionDeleteResponses = {
/**
* Successfully deleted session
@@ -1426,6 +1450,19 @@ export type SessionGetData = {
url: "/session/{id}"
}
export type SessionGetErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type SessionGetError = SessionGetErrors[keyof SessionGetErrors]
export type SessionGetResponses = {
/**
* Get session
@@ -1448,6 +1485,19 @@ export type SessionUpdateData = {
url: "/session/{id}"
}
export type SessionUpdateErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type SessionUpdateError = SessionUpdateErrors[keyof SessionUpdateErrors]
export type SessionUpdateResponses = {
/**
* Successfully updated session
@@ -1468,6 +1518,19 @@ export type SessionChildrenData = {
url: "/session/{id}/children"
}
export type SessionChildrenErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type SessionChildrenError = SessionChildrenErrors[keyof SessionChildrenErrors]
export type SessionChildrenResponses = {
/**
* List of children
@@ -1491,6 +1554,19 @@ export type SessionTodoData = {
url: "/session/{id}/todo"
}
export type SessionTodoErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type SessionTodoError = SessionTodoErrors[keyof SessionTodoErrors]
export type SessionTodoResponses = {
/**
* Todo list
@@ -1518,6 +1594,19 @@ export type SessionInitData = {
url: "/session/{id}/init"
}
export type SessionInitErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type SessionInitError = SessionInitErrors[keyof SessionInitErrors]
export type SessionInitResponses = {
/**
* 200
@@ -1560,6 +1649,19 @@ export type SessionAbortData = {
url: "/session/{id}/abort"
}
export type SessionAbortErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type SessionAbortError = SessionAbortErrors[keyof SessionAbortErrors]
export type SessionAbortResponses = {
/**
* Aborted session
@@ -1580,6 +1682,19 @@ export type SessionUnshareData = {
url: "/session/{id}/share"
}
export type SessionUnshareErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type SessionUnshareError = SessionUnshareErrors[keyof SessionUnshareErrors]
export type SessionUnshareResponses = {
/**
* Successfully unshared session
@@ -1600,6 +1715,19 @@ export type SessionShareData = {
url: "/session/{id}/share"
}
export type SessionShareErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type SessionShareError = SessionShareErrors[keyof SessionShareErrors]
export type SessionShareResponses = {
/**
* Successfully shared session
@@ -1626,6 +1754,19 @@ export type SessionSummarizeData = {
url: "/session/{id}/summarize"
}
export type SessionSummarizeErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type SessionSummarizeError = SessionSummarizeErrors[keyof SessionSummarizeErrors]
export type SessionSummarizeResponses = {
/**
* Summarized session
@@ -1649,6 +1790,19 @@ export type SessionMessagesData = {
url: "/session/{id}/message"
}
export type SessionMessagesErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type SessionMessagesError = SessionMessagesErrors[keyof SessionMessagesErrors]
export type SessionMessagesResponses = {
/**
* List of messages
@@ -1687,6 +1841,19 @@ export type SessionPromptData = {
url: "/session/{id}/message"
}
export type SessionPromptErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type SessionPromptError = SessionPromptErrors[keyof SessionPromptErrors]
export type SessionPromptResponses = {
/**
* Created message
@@ -1717,6 +1884,19 @@ export type SessionMessageData = {
url: "/session/{id}/message/{messageID}"
}
export type SessionMessageErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type SessionMessageError = SessionMessageErrors[keyof SessionMessageErrors]
export type SessionMessageResponses = {
/**
* Message
@@ -1749,6 +1929,19 @@ export type SessionCommandData = {
url: "/session/{id}/command"
}
export type SessionCommandErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type SessionCommandError = SessionCommandErrors[keyof SessionCommandErrors]
export type SessionCommandResponses = {
/**
* Created message
@@ -1778,6 +1971,19 @@ export type SessionShellData = {
url: "/session/{id}/shell"
}
export type SessionShellErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type SessionShellError = SessionShellErrors[keyof SessionShellErrors]
export type SessionShellResponses = {
/**
* Created message
@@ -1801,6 +2007,19 @@ export type SessionRevertData = {
url: "/session/{id}/revert"
}
export type SessionRevertErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type SessionRevertError = SessionRevertErrors[keyof SessionRevertErrors]
export type SessionRevertResponses = {
/**
* Updated session
@@ -1821,6 +2040,19 @@ export type SessionUnrevertData = {
url: "/session/{id}/unrevert"
}
export type SessionUnrevertErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type SessionUnrevertError = SessionUnrevertErrors[keyof SessionUnrevertErrors]
export type SessionUnrevertResponses = {
/**
* Updated session
@@ -1844,6 +2076,20 @@ export type PostSessionIdPermissionsPermissionIdData = {
url: "/session/{id}/permissions/{permissionID}"
}
export type PostSessionIdPermissionsPermissionIdErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}
export type PostSessionIdPermissionsPermissionIdError =
PostSessionIdPermissionsPermissionIdErrors[keyof PostSessionIdPermissionsPermissionIdErrors]
export type PostSessionIdPermissionsPermissionIdResponses = {
/**
* Permission processed successfully
@@ -2052,6 +2298,15 @@ export type AppLogData = {
url: "/log"
}
export type AppLogErrors = {
/**
* Bad request
*/
400: BadRequestError
}
export type AppLogError = AppLogErrors[keyof AppLogErrors]
export type AppLogResponses = {
/**
* Log entry written successfully
@@ -2106,6 +2361,15 @@ export type TuiAppendPromptData = {
url: "/tui/append-prompt"
}
export type TuiAppendPromptErrors = {
/**
* Bad request
*/
400: BadRequestError
}
export type TuiAppendPromptError = TuiAppendPromptErrors[keyof TuiAppendPromptErrors]
export type TuiAppendPromptResponses = {
/**
* Prompt processed successfully
@@ -2234,6 +2498,15 @@ export type TuiExecuteCommandData = {
url: "/tui/execute-command"
}
export type TuiExecuteCommandErrors = {
/**
* Bad request
*/
400: BadRequestError
}
export type TuiExecuteCommandError = TuiExecuteCommandErrors[keyof TuiExecuteCommandErrors]
export type TuiExecuteCommandResponses = {
/**
* Command executed successfully
@@ -2280,7 +2553,7 @@ export type AuthSetErrors = {
/**
* Bad request
*/
400: _Error
400: BadRequestError
}
export type AuthSetError = AuthSetErrors[keyof AuthSetErrors]
+1 -1
View File
@@ -51,7 +51,7 @@ export async function createOpencodeServer(options?: ServerOptions) {
throw new Error(`Failed to parse server url from output: ${line}`)
}
clearTimeout(id)
resolve(match[1])
resolve(match[1]!)
return
}
}
+3
View File
@@ -0,0 +1,3 @@
SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_SIGNING_SECRET=your-signing-secret
SLACK_APP_TOKEN=xapp-your-app-token
+4
View File
@@ -0,0 +1,4 @@
node_modules
dist
.env
.DS_Store

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