Compare commits

..
222 Commits
Author SHA1 Message Date
opencode a34e67b518 release: v1.0.153
Update Nix Hashes / update (push) Has been cancelled
2025-12-14 19:04:01 +00:00
Aiden Cline 0c7f0cfa2e tweak: fallback to provider default for temperature 2025-12-14 11:57:12 -06:00
GitHub Action 10ee6d345b chore: format code 2025-12-14 17:48:34 +00:00
Nalin SinghandGitHub 48ec68730f fix: ensure input borders are drawn in transparent themes (#5524) 2025-12-14 11:48:02 -06:00
GitHub Action 70e4efe429 chore: format code 2025-12-14 17:46:46 +00:00
92948ed8a4 feat: add server_error, rate_limit, and no_kv_space retry logic to accommodate Foundry API issues (#5527)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-14 11:46:14 -06:00
shekohexandGitHub 6d412d8872 docs: add opencode-pty and opencode-google-antigravity-auth plugins to the echosystem (#5530) 2025-12-14 11:38:53 -06:00
Lawrence SarpongandGitHub e6a0a005d6 Add Gleam LSP and formatter (#5514) 2025-12-14 10:51:08 -06:00
AymvnandGitHub 90d44751e7 docs: Fix Wakatime link in ecosystem documentation (#5528) 2025-12-14 10:49:35 -06:00
GitHub Action 4d062ba1b2 ignore: update download stats 2025-12-14 2025-12-14 12:04:18 +00:00
Aiden Cline f8bca50f00 rm unnecessary code 2025-12-13 23:45:15 -06:00
Aiden Cline 3d2ef28fa8 add topK function to transform, add temp defaults for glm and minimax 2025-12-13 23:27:11 -06:00
YeonGyu-KimandGitHub 210b3e905b fix(ui): guard Node reference for SSR compatibility in isTriggerTitle (#5509) 2025-12-13 22:28:14 -06:00
Brendan Allan 96975ef8d6 tauri: change mainBinaryName to just OpenCode 2025-12-14 12:08:53 +08:00
Brendan Allan b8b998be56 tauri: bring back appimage 2025-12-14 12:00:14 +08:00
SachnunandGitHub d8ac35f6e5 fix(tui): open parent session instead of subagent on continue flag (#5503) 2025-12-13 21:09:42 -06:00
Zhou RuiandGitHub ed1eacfce0 docs: Add opencode-websearch-cited to plugin list (#5501) 2025-12-13 20:28:39 -06:00
Adam 629f475f63 fix: sort models 2025-12-13 20:25:24 -06:00
Adam 43a7c1dd8c fix: use opencode icon 2025-12-13 20:25:24 -06:00
Adam e288ce0fca chore: cleanup 2025-12-13 20:25:24 -06:00
Adam 67b3fcb31a chore: cleanup 2025-12-13 20:25:24 -06:00
aedb5550a8 fix: limit LSP diagnostics to prevent context window waste (#5480)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-13 19:56:26 -06:00
Aiden Cline 1638ffde69 docs: networking 2025-12-13 18:20:44 -06:00
Aiden Cline d4cfbd8219 chore: reduce duplication of field in transform 2025-12-13 18:07:22 -06:00
Adam c7bac83212 chore: cleanup 2025-12-13 16:17:32 -06:00
Adam fc9789d7a7 fix(desktop): archive button 2025-12-13 16:14:31 -06:00
Adam a8957d8d16 fix(desktop): auto scroll 2025-12-13 15:56:12 -06:00
Adam 0660433921 feat(desktop): show richer status when thinking 2025-12-13 15:47:50 -06:00
Adam 1a6f4f1c0d fix: css scroll jitter 2025-12-13 15:36:28 -06:00
Adam 974a24ba02 fix: don't rotate placeholders in session 2025-12-13 15:25:56 -06:00
Adam 5ebe29de1e fix: don't open shell by default 2025-12-13 15:17:22 -06:00
Github Action 6bdf8b1fe1 Update Nix flake.lock and hashes 2025-12-13 21:13:53 +00:00
Adam 5bcc93851c chore: cleanup 2025-12-13 15:12:41 -06:00
Adam d0789632b4 fix(desktop): terminal light mode 2025-12-13 15:12:32 -06:00
Adam a6e297baad feat(desktop): message history 2025-12-13 14:57:24 -06:00
Adam 307af10c8b fix: session turn scroll 2025-12-13 14:57:23 -06:00
Felipe Oduardo SierraandGitHub f254cf76d9 add ARM64 Docker image support (#5483) 2025-12-13 13:01:59 -06:00
Github Action b4ffaa21ec Update Nix flake.lock and hashes 2025-12-13 19:01:20 +00:00
Aiden Cline 7bf6f264e4 bump bun version & set flags this time 2025-12-13 13:00:03 -06:00
GitHub Action 7434fbba8e chore: format code 2025-12-13 17:34:07 +00:00
Jan-Niklas W.andGitHub b7581e01ea docs: fix title for JetBrains ACP config file (#5479) 2025-12-13 11:33:31 -06:00
YeonGyu-KimandGitHub b46d4789fc docs: add oh-my-opencode to plugins list (#5481) 2025-12-13 11:33:10 -06:00
GitHub Action 199bd8a9a2 chore: format code 2025-12-13 17:30:48 +00:00
decf2452c4 feat: add dockerfile language server (#5252)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-13 11:30:15 -06:00
GitHub Action d8663a44c2 ignore: update download stats 2025-12-13 2025-12-13 12:04:19 +00:00
rari404andGitHub 8917a4c609 feat: add texlab language server and latexindent formatter (#5251) 2025-12-12 23:50:09 -06:00
GitHub Action 5d7a52f8b8 chore: format code 2025-12-13 02:09:41 +00:00
Jan-Niklas W.andGitHub b7b827c5bd docs: JetBrains IDEs to ACP config docs page (#5465) 2025-12-12 20:09:08 -06:00
Matt SilverlockandGitHub 613e082358 github: support GITHUB_TOKEN + skip OIDC (#5459) 2025-12-12 19:55:46 -06:00
b6856bd593 fix: add --session flag to attach command (#5460)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-12 19:45:28 -06:00
David Hill 7cb5a77ba6 fix: mute the project path in the sidebar that proceeds the final directory 2025-12-12 23:45:39 +00:00
David Hill cd9898a565 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-12-12 23:25:11 +00:00
Dax Raad a4ffa869cc fix 2025-12-12 18:15:31 -05:00
David Hill dbc84ff4c3 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-12-12 22:50:50 +00:00
David Hill c11ea3fd92 fix: mute the whole prompt area when leader key is active 2025-12-12 22:50:48 +00:00
GitHub Action 3c3a0f8afb chore: format code 2025-12-12 22:48:43 +00:00
Aiden Cline b93614cb81 docs: add env vars sections 2025-12-12 16:47:50 -06:00
opencode b84d513bd7 release: v1.0.152
Update Nix Hashes / update (push) Has been cancelled
2025-12-12 22:29:21 +00:00
Adam 0554d03162 Revert "fix: archive button"
This reverts commit bc3286de46.
2025-12-12 16:16:52 -06:00
15caecdb45 shell tweaks, better handling for windows (#5455)
Co-authored-by: GitHub Action <action@github.com>
2025-12-12 16:11:07 -06:00
Adam 91ab966921 fix: max height on bash tool 2025-12-12 16:10:13 -06:00
Adam bc3286de46 fix: archive button 2025-12-12 16:03:07 -06:00
Dax Raad af45444496 desktop: fix build on Linux and Windows by making macOS title bar styling conditional 2025-12-12 16:47:48 -05:00
Sebastian Herrlinger 43202f2820 only exit app when prompt is empty, otherwise fallthrough, fix #5457 2025-12-12 22:45:28 +01:00
GitHub Action ce37e11bfe chore: format code 2025-12-12 21:44:09 +00:00
Dan BrownandGitHub 6e9833acce Shell: No -l in fallback, for max compatibility (#5452) 2025-12-12 15:43:35 -06:00
opencode 379c4ecab3 release: v1.0.151
Update Nix Hashes / update (push) Has been cancelled
2025-12-12 21:34:32 +00:00
Github Action f1db4b60c4 Update Nix flake.lock and hashes 2025-12-12 21:28:16 +00:00
Adam 9846b26be7 fix: desktop layout 2025-12-12 15:26:53 -06:00
Adam d6ba6af6f3 fix: desktop layout 2025-12-12 15:24:43 -06:00
Adam d463ade028 fix: desktop layout 2025-12-12 15:24:43 -06:00
Adam 6c3495a75a fix: desktop layout 2025-12-12 15:24:43 -06:00
Adam a16edb4ea0 fix: desktop layout 2025-12-12 15:24:43 -06:00
Adam 9efe09564b fix: desktop layout 2025-12-12 15:24:42 -06:00
Adam ccdd77032a fix: desktop layout 2025-12-12 15:24:42 -06:00
Adam 41e234c6d0 fix: desktop layout 2025-12-12 15:24:42 -06:00
Adam 3e03646e42 fix: desktop layout 2025-12-12 15:24:42 -06:00
Adam f7acc34327 fix: desktop layout 2025-12-12 15:24:41 -06:00
Adam bf420e7df6 chore: cleanup 2025-12-12 15:24:41 -06:00
Adam 78484f545c chore: cleanup 2025-12-12 15:24:41 -06:00
Adam ad008d2151 wip: desktop timeline changes 2025-12-12 15:24:41 -06:00
Dax Raad 651a10d6db only gen summary if diffs 2025-12-12 16:24:26 -05:00
Aiden Cline f9674793af tweak: 5.1 -> 5. in transform 2025-12-12 15:17:11 -06:00
GitHub Action f3a33d41f1 chore: format code 2025-12-12 20:26:02 +00:00
David Hill 642eec3dfd Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-12-12 20:25:21 +00:00
Dax Raad 73513612d4 reuse existing server query 2025-12-12 15:23:38 -05:00
David Hill 9b77246246 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-12-12 20:23:06 +00:00
David Hill cf3bc1e0a6 fix: increase font-size-small to 13px 2025-12-12 20:22:51 +00:00
David Hill 4550ad049e fix: make syntax colors have more contrast 2025-12-12 20:22:08 +00:00
Dax Raad d51c6ca39f sync 2025-12-12 15:21:29 -05:00
Dax Raad 47c6a2430c sync 2025-12-12 15:18:07 -05:00
Aiden Cline 909013320b tweak: 5.1 -> 5. reasoning effort match 2025-12-12 13:21:57 -06:00
Frank 770a4d87db Zen: add gpt5.2 2025-12-12 13:48:54 -05:00
Luke ParkerandGitHub 2e417c4d8c fix: osascript for clipboard typo (#5430) 2025-12-12 11:37:43 -06:00
Adam 2da527aaa6 fix: desktop layout 2025-12-12 11:11:17 -06:00
Jeremy OsihandGitHub 0303eb0cc1 Change tooltip text from 'Open file' to 'New Terminal' (#5435) 2025-12-12 10:57:52 -06:00
Adam 3f4a792c8a fix: tweak missing colors 2025-12-12 10:56:30 -06:00
Adam 9629f7464b chore: cleanup 2025-12-12 10:45:34 -06:00
xu0o0andGitHub 9017d10303 acp: fix internal error on /compact (#5424) 2025-12-12 10:43:18 -06:00
Brendan AllanandGitHub 59d4041aa4 tauri: add basic custom titlebar (#5438) 2025-12-12 09:37:17 -06:00
Adam feb8c4f3c6 feat(desktop): archive sessions 2025-12-12 07:07:54 -06:00
Adam 3f5cd2c4a8 fix(desktop): audio stuff 2025-12-12 06:21:31 -06:00
Adam a160eb76df fix(desktop): homedir aware path on home 2025-12-12 06:21:31 -06:00
GitHub Action e4b2028f91 ignore: update download stats 2025-12-12 2025-12-12 12:04:34 +00:00
Github Action ffc48e115b Update Nix flake.lock and hashes 2025-12-12 11:16:22 +00:00
Adam 04b4dacee3 feat(desktop): basic alerting 2025-12-12 05:14:51 -06:00
GitHub Action c0e30f48c6 chore: format code 2025-12-12 09:44:46 +00:00
David Hill 99158e736b Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-12-12 09:44:06 +00:00
David Hill 4c02d515a1 wip: desktop sidebar icon updates 2025-12-12 09:44:04 +00:00
David Hill b803a9732d fix: make the logo on the home screen non-selectable 2025-12-12 09:43:41 +00:00
Brendan Allan f9d5e18790 tauri: create window with full screen size 2025-12-12 17:04:00 +08:00
Github Action 147111c9c6 Update Nix flake.lock and hashes 2025-12-12 09:00:19 +00:00
GitHub Action 9a70eb538b chore: format code 2025-12-12 08:59:18 +00:00
Brendan Allan 0b1731142e tauri: initialise store and window-state plugins 2025-12-12 16:58:37 +08:00
Viktor ForsmanandGitHub 7ec48dfd15 fix: debug lsp diagnostics cmd for certain lsps (#5420) 2025-12-11 23:20:38 -06:00
Frank 57120e69ed Zen: sync 2025-12-11 23:41:04 -05:00
Rhys SullivanandGitHub 11efda3f5c [feat]: show indicator for in progress chats in the sessions list (#5417) 2025-12-11 21:57:03 -05:00
SachnunandGitHub a5cb4e41f5 fix(tui): restore input on timeline revert and show newest first (#5366) 2025-12-11 20:07:31 -06:00
GitHub Action 88b2382b97 chore: format code 2025-12-12 02:06:49 +00:00
SachnunandGitHub 237c0253c2 fix(server): make time field optional in session update validator (#5372) 2025-12-11 20:06:15 -06:00
xu0o0andGitHub a9f27371cf acp: replay conversation history in session/load (#5385) 2025-12-11 20:02:06 -06:00
opencode-agent[bot]GitHubopencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>rekram1-node
9c126c5b64 Removed cache mention from webfetch prompt. (#5412)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-12-11 19:53:39 -06:00
Zeno JiricekandGitHub e241aa21b9 docs: Add opencode-md-table-formatter and plugin template (#5405) 2025-12-11 17:02:49 -06:00
Koichi NakayamadaandGitHub 1fd005838c fix(tui): ensure fatal error UI is readable in light mode (#5387) 2025-12-11 16:39:27 -06:00
Koichi NakayamadaandGitHub 61ba844234 fix: add fg color to some TUI text elements for light mode visibility (#5378) 2025-12-11 16:29:34 -06:00
AdánandGitHub ffec52a17c fix: Windows LSP URIs using backslashes (Biome initialization failure) (#5317) 2025-12-11 16:28:39 -06:00
Aiden Cline 342595e0f7 tweak: setting chat_template_args in transform 2025-12-11 16:22:10 -06:00
Aiden Cline b41051b4ee tweak(tui): better error msg rendering 2025-12-11 16:22:10 -06:00
Dax Raad 6f8746ab94 console: fix download route async handling 2025-12-11 16:55:23 -05:00
Dax Raad 9d4ed5b04a console: fix download links to use dynamic GitHub release version 2025-12-11 16:49:15 -05:00
Adam e149b7c1e2 fix: avatar colors 2025-12-11 15:39:41 -06:00
GitHub Action 55957b2ac7 chore: format code 2025-12-11 21:34:17 +00:00
Jay V 14291bff71 docs: lander copy 2025-12-11 16:26:42 -05:00
Adam a0472c0312 fix: free model logic 2025-12-11 15:24:32 -06:00
Dax Raad 0400024d02 core: fix missing Flag import in session prompt 2025-12-11 16:18:04 -05:00
Dax Raad d4dc142cc2 core: add client identification to user agent and request headers for better tracking 2025-12-11 16:18:04 -05:00
Adam bfdb236581 fix: toast colors 2025-12-11 15:16:44 -06:00
Aiden Cline 4e92f54415 tweak: bash tool, try to prevent the cd spam 2025-12-11 14:58:46 -06:00
Adam f8dc740c61 fix: remove settings button 2025-12-11 14:55:41 -06:00
Adam dea5111a5a fix: message nav popover placement 2025-12-11 14:52:06 -06:00
Aiden Cline 4f5abe387d tweak: bash tool deny msg 2025-12-11 14:46:58 -06:00
Adam 7d55aeee0a fix: no loading state in message nav 2025-12-11 14:46:32 -06:00
Adam b34f434332 fix: message order ascending 2025-12-11 14:46:32 -06:00
Jorgen HenriksenGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2a9269c347 add experimental.chat.messages.transform hook (#5207)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-11 14:38:45 -06:00
SachnunandGitHub 0efdc3a8a0 feat: add caching for opencode binary in GitHub Actions (#5384) 2025-12-11 14:14:37 -06:00
GitHub Action fc9dfd054a chore: format code 2025-12-11 20:14:20 +00:00
Dax Raad 013bf079cc ignore: added debug logging for share data loading performance 2025-12-11 15:13:36 -05:00
opencode 5ffcde9dba release: v1.0.150
Update Nix Hashes / update (push) Has been cancelled
2025-12-11 20:09:20 +00:00
Dax Raad e0f885ffc8 ci 2025-12-11 14:57:54 -05:00
Aiden Cline 5b21334fdd fix 2025-12-11 13:55:08 -06:00
Shantur RathoreandGitHub 755a79cd8e fix: Writing to Bun.stderr causes CPU pinning to 100%. (#5396) 2025-12-11 13:47:59 -06:00
Adam 16b7370d8c wip(desktop): progress 2025-12-11 13:42:47 -06:00
Adam 634fd62b25 wip(desktop): progress 2025-12-11 13:42:47 -06:00
Adam e845eedbc3 wip(desktop): progress 2025-12-11 13:42:47 -06:00
Adam 4ae7e1b19c wip(desktop): progress 2025-12-11 13:42:46 -06:00
Adam 0ca758e135 wip(desktop): progress 2025-12-11 13:42:45 -06:00
Dax Raad ea8508ee44 ci 2025-12-11 14:38:45 -05:00
GitHub Action 78d4f32d79 chore: format code 2025-12-11 19:35:36 +00:00
David Hill afcd547a16 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-12-11 19:34:34 +00:00
David Hill 521fbb93cd wip: lander updates 2025-12-11 19:33:55 +00:00
Dax Raad 9d73096db0 ci: fix Ubuntu host detection in Tauri build workflow 2025-12-11 13:27:21 -05:00
GitHub Action c11bb440e6 chore: format code 2025-12-11 18:21:10 +00:00
Sebastian Herrlinger 80e04be84f fix super modifier parsing 2025-12-11 19:20:23 +01:00
Dax Raad 639320b3e1 ci: install libglib2.0-dev on Blacksmith Linux runners to fix missing glib-2.0 system library error 2025-12-11 13:16:44 -05:00
GitHub Action 55ea20de84 chore: format code 2025-12-11 18:12:25 +00:00
Dax Raad 21cf3a7c1b ci 2025-12-11 13:11:36 -05:00
David Hill 4f855072f2 wip: avatar colors 2025-12-11 18:07:14 +00:00
David Hill 11e6a181ad fix: free cta 2025-12-11 18:07:14 +00:00
GitHub Action 4c9208fbf1 chore: format code 2025-12-11 18:05:35 +00:00
Dax Raad 3a9bbe2371 ci: use Blacksmith runners for Linux and Windows Tauri builds to improve build performance 2025-12-11 13:04:55 -05:00
opencode 6382bda7d6 release: v1.0.149
Update Nix Hashes / update (push) Has been cancelled
2025-12-11 18:00:30 +00:00
Dax Raad 885a142ae3 temporarily remove undo/redo keybinds 2025-12-11 12:55:08 -05:00
GitHub Actionandopencode 4387602f9d chore: format code 2025-12-11 17:49:43 +00:00
opencode 71fc8b2115 release: v1.0.148
Update Nix Hashes / update (push) Has been cancelled
2025-12-11 17:49:42 +00:00
Dax Raad 137716e0dc ci: fix tauri release asset naming to use opencode-desktop-[platform]-[arch][ext] format 2025-12-11 12:45:48 -05:00
Dax Raad 95526fb9ed fix share image 2025-12-11 12:20:11 -05:00
GitHub Actionandopencode a5cc19068b chore: format code 2025-12-11 16:46:43 +00:00
opencode 48c7913431 release: v1.0.147
Update Nix Hashes / update (push) Has been cancelled
2025-12-11 16:46:43 +00:00
Dax Raad 89d9856ed2 ci: enable tauri desktop app to be published with GitHub release ID and tag name 2025-12-11 11:41:54 -05:00
Sebastian Herrlinger c12be73bf7 align default input keybinds 2025-12-11 17:39:29 +01:00
Github Action de6fb3126c Update Nix flake.lock and hashes 2025-12-11 16:25:22 +00:00
GitHub Action fad5cbe6c7 chore: format code 2025-12-11 16:24:30 +00:00
Sebastian Herrlinger e271852bc3 allow custom mappings for all textarea actions via config 2025-12-11 17:23:47 +01:00
David Hill 4e02704f17 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-12-11 16:21:32 +00:00
David Hill b17fdc7f4e fix: inverted text colors 2025-12-11 16:21:31 +00:00
Dax Raad df4d9236a6 fix bug causing windows builds to sometimes fail 2025-12-11 11:01:43 -05:00
GDR!andGitHub 0ee626ba9f Add ripgrep to Docker image (#5379) 2025-12-11 09:47:56 -06:00
Daniel PolitoandGitHub dce4585d40 Fix / Improve Github Eyes Reaction Removal (#5383) 2025-12-11 09:42:10 -06:00
David Hill 5ffc1617fe Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-12-11 14:37:05 +00:00
David Hill 93dd01947d wip: update lander video 2025-12-11 14:36:49 +00:00
Brendan Allan 5e7d908dc9 tauri: always build cli in predev 2025-12-11 22:20:22 +08:00
David Hill eb6596cb97 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-12-11 13:10:46 +00:00
David Hill 8fcd31f353 wip: social share image 2025-12-11 13:03:08 +00:00
Adam dfdd1c9b20 fix: load fonts the right way 2025-12-11 06:51:39 -06:00
Adam d7b8dce6a7 fix: missing font 2025-12-11 06:49:00 -06:00
Adam f9f78122d0 wip(desktop): progress 2025-12-11 06:48:59 -06:00
Adam 1980113ee4 wip(desktop): progress 2025-12-11 06:48:59 -06:00
Adam 85c0311d38 wip(desktop): progress 2025-12-11 06:48:59 -06:00
Adam 740aa8e541 wip(desktop): progress 2025-12-11 06:48:59 -06:00
Adam 1b331548ba wip(desktop): progress 2025-12-11 06:48:59 -06:00
Adam 3bb546c94d wip(desktop): progress 2025-12-11 06:48:58 -06:00
David Hill 8e15bcb68e Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-12-11 12:40:24 +00:00
David Hill ca08dc87dd wip: add faq to download 2025-12-11 12:38:40 +00:00
David Hill 4287552991 wip: add more items to download 2025-12-11 12:26:47 +00:00
GitHub Action f1af54ada1 chore: format code 2025-12-11 12:12:13 +00:00
David Hill 577cacb7db Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-12-11 12:11:36 +00:00
David Hill 8346550d26 fix: change terminal title to OpenCode 2025-12-11 12:10:04 +00:00
David Hill a8e3caca3f wip: lander updates 2025-12-11 12:09:31 +00:00
GitHub Action fd83578442 ignore: update download stats 2025-12-11 2025-12-11 12:04:43 +00:00
David Hill e91d121ef8 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-12-11 10:47:24 +00:00
David Hill 1b4975ba54 wip: hero and nav updates 2025-12-11 10:47:22 +00:00
David Hill 380d2c466e wip: lander hero updates 2025-12-11 10:36:31 +00:00
Brendan Allan 53d2edc0f2 write typescript artifacts to node_modules/.ts-dist 2025-12-11 18:35:07 +08:00
David Hill 02705e460f wip: downloads 2025-12-11 10:19:41 +00:00
GitHub Action 44cd384e3c chore: format code 2025-12-11 06:33:51 +00:00
Aiden ClineandGitHub 8ac5204009 ignore: Comment out plugin entry in opencode.jsonc
Comment out the plugin entry in opencode.jsonc
2025-12-11 00:33:17 -06:00
opencode ddd5464081 release: v1.0.146
Update Nix Hashes / update (push) Has been cancelled
2025-12-11 06:23:53 +00:00
David Hill a5a3060208 wip: lander updates 2025-12-11 00:56:05 +00:00
David Hill a468044c9f Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-12-11 00:46:07 +00:00
David Hill f0274fd29f wip: lander updates 2025-12-11 00:46:02 +00:00
David Hill e9d3e240c2 wip: lander updates 2025-12-11 00:18:22 +00:00
David Hill fba0aad2f8 wip: lander updates 2025-12-10 23:57:11 +00:00
188 changed files with 6376 additions and 1853 deletions
+21 -10
View File
@@ -31,7 +31,7 @@ permissions:
jobs:
publish:
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.repository == 'sst/opencode' && github.ref == 'refs/heads/dev'
if: github.repository == 'sst/opencode'
steps:
- uses: actions/checkout@v3
with:
@@ -64,14 +64,20 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- uses: actions/setup-node@v4
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Publish
run: |
./script/publish.ts
id: publish
run: ./script/publish.ts
env:
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_VERSION: ${{ inputs.version }}
@@ -79,9 +85,12 @@ jobs:
AUR_KEY: ${{ secrets.AUR_KEY }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
NPM_CONFIG_PROVENANCE: false
outputs:
releaseId: ${{ steps.publish.outputs.releaseId }}
tagName: ${{ steps.publish.outputs.tagName }}
publish-tauri:
if: false # inputs.bump || inputs.version
needs: publish
continue-on-error: true
strategy:
fail-fast: false
@@ -91,9 +100,9 @@ jobs:
target: x86_64-apple-darwin
- host: macos-latest
target: aarch64-apple-darwin
- host: windows-latest
- host: blacksmith-4vcpu-windows-2025
target: x86_64-pc-windows-msvc
- host: ubuntu-24.04
- host: blacksmith-4vcpu-ubuntu-2404
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.settings.host }}
steps:
@@ -126,7 +135,7 @@ jobs:
- uses: ./.github/actions/setup-bun
- name: install dependencies (ubuntu only)
if: startsWith(matrix.settings.host, 'ubuntu')
if: contains(matrix.settings.host, 'ubuntu')
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
@@ -158,7 +167,7 @@ jobs:
# Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
- run: cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage
if: startsWith(matrix.settings.host, 'ubuntu')
if: contains(matrix.settings.host, 'ubuntu')
- name: Build and upload artifacts
uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
@@ -176,7 +185,9 @@ jobs:
with:
projectPath: packages/tauri
uploadWorkflowArtifacts: true
tauriScript: ${{ (startsWith(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
args: --target ${{ matrix.settings.target }}
updaterJsonPreferNsis: true
# releaseId: TODO
releaseId: ${{ needs.publish.outputs.releaseId }}
tagName: ${{ needs.publish.outputs.tagName }}
releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-openai-codex-auth"],
// "plugin": ["opencode-openai-codex-auth"],
// "enterprise": {
// "url": "https://enterprise.dev.opencode.ai",
// },
+170 -166
View File
@@ -1,168 +1,172 @@
# Download Stats
| Date | GitHub Downloads | npm Downloads | Total |
| ---------- | ------------------- | ----------------- | ------------------- |
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) |
| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) |
| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) |
| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) |
| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) |
| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) |
| 2025-12-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) |
| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773) | 1,985,410 (+27,363) |
| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
| Date | GitHub Downloads | npm Downloads | Total |
| ---------- | ------------------- | ------------------- | ------------------- |
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
| 2025-08-13 | 182,997 (+6,690) | 177,182 (+5,306) | 360,179 (+11,996) |
| 2025-08-14 | 189,063 (+6,066) | 179,741 (+2,559) | 368,804 (+8,625) |
| 2025-08-15 | 193,608 (+4,545) | 181,792 (+2,051) | 375,400 (+6,596) |
| 2025-08-16 | 198,118 (+4,510) | 184,558 (+2,766) | 382,676 (+7,276) |
| 2025-08-17 | 201,299 (+3,181) | 186,269 (+1,711) | 387,568 (+4,892) |
| 2025-08-18 | 204,559 (+3,260) | 187,399 (+1,130) | 391,958 (+4,390) |
| 2025-08-19 | 209,814 (+5,255) | 189,668 (+2,269) | 399,482 (+7,524) |
| 2025-08-20 | 214,497 (+4,683) | 191,481 (+1,813) | 405,978 (+6,496) |
| 2025-08-21 | 220,465 (+5,968) | 194,784 (+3,303) | 415,249 (+9,271) |
| 2025-08-22 | 225,899 (+5,434) | 197,204 (+2,420) | 423,103 (+7,854) |
| 2025-08-23 | 229,005 (+3,106) | 199,238 (+2,034) | 428,243 (+5,140) |
| 2025-08-24 | 232,098 (+3,093) | 201,157 (+1,919) | 433,255 (+5,012) |
| 2025-08-25 | 236,607 (+4,509) | 202,650 (+1,493) | 439,257 (+6,002) |
| 2025-08-26 | 242,783 (+6,176) | 205,242 (+2,592) | 448,025 (+8,768) |
| 2025-08-27 | 248,409 (+5,626) | 205,242 (+0) | 453,651 (+5,626) |
| 2025-08-28 | 252,796 (+4,387) | 205,242 (+0) | 458,038 (+4,387) |
| 2025-08-29 | 256,045 (+3,249) | 211,075 (+5,833) | 467,120 (+9,082) |
| 2025-08-30 | 258,863 (+2,818) | 212,397 (+1,322) | 471,260 (+4,140) |
| 2025-08-31 | 262,004 (+3,141) | 213,944 (+1,547) | 475,948 (+4,688) |
| 2025-09-01 | 265,359 (+3,355) | 215,115 (+1,171) | 480,474 (+4,526) |
| 2025-09-02 | 270,483 (+5,124) | 217,075 (+1,960) | 487,558 (+7,084) |
| 2025-09-03 | 274,793 (+4,310) | 219,755 (+2,680) | 494,548 (+6,990) |
| 2025-09-04 | 280,430 (+5,637) | 222,103 (+2,348) | 502,533 (+7,985) |
| 2025-09-05 | 283,769 (+3,339) | 223,793 (+1,690) | 507,562 (+5,029) |
| 2025-09-06 | 286,245 (+2,476) | 225,036 (+1,243) | 511,281 (+3,719) |
| 2025-09-07 | 288,623 (+2,378) | 225,866 (+830) | 514,489 (+3,208) |
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |
| 2025-09-12 | 321,046 (+6,963) | 240,728 (+3,372) | 561,774 (+10,335) |
| 2025-09-13 | 324,894 (+3,848) | 245,539 (+4,811) | 570,433 (+8,659) |
| 2025-09-14 | 328,876 (+3,982) | 248,245 (+2,706) | 577,121 (+6,688) |
| 2025-09-15 | 334,201 (+5,325) | 250,983 (+2,738) | 585,184 (+8,063) |
| 2025-09-16 | 342,609 (+8,408) | 255,264 (+4,281) | 597,873 (+12,689) |
| 2025-09-17 | 351,117 (+8,508) | 260,970 (+5,706) | 612,087 (+14,214) |
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
| 2025-09-24 | 393,325 (+6,317) | 294,927 (+5,798) | 688,252 (+12,115) |
| 2025-09-25 | 398,879 (+5,554) | 301,663 (+6,736) | 700,542 (+12,290) |
| 2025-09-26 | 404,334 (+5,455) | 306,713 (+5,050) | 711,047 (+10,505) |
| 2025-09-27 | 411,618 (+7,284) | 317,763 (+11,050) | 729,381 (+18,334) |
| 2025-09-28 | 414,910 (+3,292) | 322,522 (+4,759) | 737,432 (+8,051) |
| 2025-09-29 | 419,919 (+5,009) | 328,033 (+5,511) | 747,952 (+10,520) |
| 2025-09-30 | 427,991 (+8,072) | 336,472 (+8,439) | 764,463 (+16,511) |
| 2025-10-01 | 433,591 (+5,600) | 341,742 (+5,270) | 775,333 (+10,870) |
| 2025-10-02 | 440,852 (+7,261) | 348,099 (+6,357) | 788,951 (+13,618) |
| 2025-10-03 | 446,829 (+5,977) | 359,937 (+11,838) | 806,766 (+17,815) |
| 2025-10-04 | 452,561 (+5,732) | 370,386 (+10,449) | 822,947 (+16,181) |
| 2025-10-05 | 455,559 (+2,998) | 374,745 (+4,359) | 830,304 (+7,357) |
| 2025-10-06 | 460,927 (+5,368) | 379,489 (+4,744) | 840,416 (+10,112) |
| 2025-10-07 | 467,336 (+6,409) | 385,438 (+5,949) | 852,774 (+12,358) |
| 2025-10-08 | 474,643 (+7,307) | 394,139 (+8,701) | 868,782 (+16,008) |
| 2025-10-09 | 479,203 (+4,560) | 400,526 (+6,387) | 879,729 (+10,947) |
| 2025-10-10 | 484,374 (+5,171) | 406,015 (+5,489) | 890,389 (+10,660) |
| 2025-10-11 | 488,427 (+4,053) | 414,699 (+8,684) | 903,126 (+12,737) |
| 2025-10-12 | 492,125 (+3,698) | 418,745 (+4,046) | 910,870 (+7,744) |
| 2025-10-14 | 505,130 (+13,005) | 429,286 (+10,541) | 934,416 (+23,546) |
| 2025-10-15 | 512,717 (+7,587) | 439,290 (+10,004) | 952,007 (+17,591) |
| 2025-10-16 | 517,719 (+5,002) | 447,137 (+7,847) | 964,856 (+12,849) |
| 2025-10-17 | 526,239 (+8,520) | 457,467 (+10,330) | 983,706 (+18,850) |
| 2025-10-18 | 531,564 (+5,325) | 465,272 (+7,805) | 996,836 (+13,130) |
| 2025-10-19 | 536,209 (+4,645) | 469,078 (+3,806) | 1,005,287 (+8,451) |
| 2025-10-20 | 541,264 (+5,055) | 472,952 (+3,874) | 1,014,216 (+8,929) |
| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
| 2025-10-25 | 578,927 (+6,235) | 516,129 (+9,224) | 1,095,056 (+15,459) |
| 2025-10-26 | 584,409 (+5,482) | 521,179 (+5,050) | 1,105,588 (+10,532) |
| 2025-10-27 | 589,999 (+5,590) | 526,001 (+4,822) | 1,116,000 (+10,412) |
| 2025-10-28 | 595,776 (+5,777) | 532,438 (+6,437) | 1,128,214 (+12,214) |
| 2025-10-29 | 606,259 (+10,483) | 542,064 (+9,626) | 1,148,323 (+20,109) |
| 2025-10-30 | 613,746 (+7,487) | 542,064 (+0) | 1,155,810 (+7,487) |
| 2025-10-30 | 617,846 (+4,100) | 555,026 (+12,962) | 1,172,872 (+17,062) |
| 2025-10-31 | 626,612 (+8,766) | 564,579 (+9,553) | 1,191,191 (+18,319) |
| 2025-11-01 | 636,100 (+9,488) | 581,806 (+17,227) | 1,217,906 (+26,715) |
| 2025-11-02 | 644,067 (+7,967) | 590,004 (+8,198) | 1,234,071 (+16,165) |
| 2025-11-03 | 653,130 (+9,063) | 597,139 (+7,135) | 1,250,269 (+16,198) |
| 2025-11-04 | 663,912 (+10,782) | 608,056 (+10,917) | 1,271,968 (+21,699) |
| 2025-11-05 | 675,074 (+11,162) | 619,690 (+11,634) | 1,294,764 (+22,796) |
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
| 2025-11-10 | 722,288 (+8,826) | 668,225 (+7,766) | 1,390,513 (+16,592) |
| 2025-11-11 | 729,769 (+7,481) | 677,501 (+9,276) | 1,407,270 (+16,757) |
| 2025-11-12 | 740,180 (+10,411) | 686,454 (+8,953) | 1,426,634 (+19,364) |
| 2025-11-13 | 749,905 (+9,725) | 696,157 (+9,703) | 1,446,062 (+19,428) |
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |
| 2025-11-21 | 826,309 (+11,689) | 769,307 (+11,400) | 1,595,616 (+23,089) |
| 2025-11-22 | 837,269 (+10,960) | 780,996 (+11,689) | 1,618,265 (+22,649) |
| 2025-11-23 | 846,609 (+9,340) | 795,069 (+14,073) | 1,641,678 (+23,413) |
| 2025-11-24 | 856,733 (+10,124) | 804,033 (+8,964) | 1,660,766 (+19,088) |
| 2025-11-25 | 869,423 (+12,690) | 817,339 (+13,306) | 1,686,762 (+25,996) |
| 2025-11-26 | 881,414 (+11,991) | 832,518 (+15,179) | 1,713,932 (+27,170) |
| 2025-11-27 | 893,960 (+12,546) | 846,180 (+13,662) | 1,740,140 (+26,208) |
| 2025-11-28 | 901,741 (+7,781) | 856,482 (+10,302) | 1,758,223 (+18,083) |
| 2025-11-29 | 908,689 (+6,948) | 863,361 (+6,879) | 1,772,050 (+13,827) |
| 2025-11-30 | 916,116 (+7,427) | 870,194 (+6,833) | 1,786,310 (+14,260) |
| 2025-12-01 | 925,898 (+9,782) | 876,500 (+6,306) | 1,802,398 (+16,088) |
| 2025-12-02 | 939,250 (+13,352) | 890,919 (+14,419) | 1,830,169 (+27,771) |
| 2025-12-03 | 952,249 (+12,999) | 903,713 (+12,794) | 1,855,962 (+25,793) |
| 2025-12-04 | 965,611 (+13,362) | 916,471 (+12,758) | 1,882,082 (+26,120) |
| 2025-12-05 | 977,996 (+12,385) | 930,616 (+14,145) | 1,908,612 (+26,530) |
| 2025-12-06 | 987,884 (+9,888) | 943,773 (+13,157) | 1,931,657 (+23,045) |
| 2025-12-07 | 994,046 (+6,162) | 951,425 (+7,652) | 1,945,471 (+13,814) |
| 2025-12-08 | 1,000,898 (+6,852) | 957,149 (+5,724) | 1,958,047 (+12,576) |
| 2025-12-09 | 1,011,488 (+10,590) | 973,922 (+16,773) | 1,985,410 (+27,363) |
| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) |
| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) |
+68 -28
View File
@@ -20,7 +20,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.0.145",
"version": "1.0.153",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -48,7 +48,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.0.145",
"version": "1.0.153",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -75,7 +75,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.0.145",
"version": "1.0.153",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -99,7 +99,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.0.145",
"version": "1.0.153",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -123,7 +123,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.0.145",
"version": "1.0.153",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -131,7 +131,9 @@
"@opencode-ai/util": "workspace:*",
"@shikijs/transformers": "3.9.2",
"@solid-primitives/active-element": "2.1.3",
"@solid-primitives/audio": "1.4.2",
"@solid-primitives/event-bus": "1.1.2",
"@solid-primitives/media": "2.3.3",
"@solid-primitives/resize-observer": "2.1.3",
"@solid-primitives/scroll": "2.1.3",
"@solid-primitives/storage": "4.3.3",
@@ -168,7 +170,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.0.145",
"version": "1.0.153",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -197,7 +199,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.0.145",
"version": "1.0.153",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
@@ -213,7 +215,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.0.145",
"version": "1.0.153",
"bin": {
"opencode": "./bin/opencode",
},
@@ -243,8 +245,8 @@
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "1.5.2",
"@opentui/core": "0.1.60",
"@opentui/solid": "0.1.60",
"@opentui/core": "0.0.0-20251211-4403a69a",
"@opentui/solid": "0.0.0-20251211-4403a69a",
"@parcel/watcher": "2.5.1",
"@pierre/precision-diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
@@ -305,7 +307,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.0.145",
"version": "1.0.153",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -325,7 +327,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.0.145",
"version": "1.0.153",
"devDependencies": {
"@hey-api/openapi-ts": "0.88.1",
"@tsconfig/node22": "catalog:",
@@ -336,7 +338,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.0.145",
"version": "1.0.153",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -349,15 +351,18 @@
},
"packages/tauri": {
"name": "@opencode-ai/tauri",
"version": "1.0.145",
"version": "1.0.153",
"dependencies": {
"@opencode-ai/desktop": "workspace:*",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-os": "~2",
"@tauri-apps/plugin-process": "~2",
"@tauri-apps/plugin-shell": "~2",
"@tauri-apps/plugin-store": "~2",
"@tauri-apps/plugin-updater": "~2",
"@tauri-apps/plugin-window-state": "~2",
"solid-js": "catalog:",
},
"devDependencies": {
@@ -371,13 +376,15 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.0.145",
"version": "1.0.153",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*",
"@pierre/precision-diffs": "catalog:",
"@shikijs/transformers": "3.9.2",
"@solid-primitives/bounds": "0.1.3",
"@solid-primitives/resize-observer": "2.1.3",
"@solidjs/meta": "catalog:",
"@typescript/native-preview": "catalog:",
"fuzzysort": "catalog:",
@@ -394,6 +401,7 @@
"@tailwindcss/vite": "catalog:",
"@tsconfig/node22": "catalog:",
"@types/bun": "catalog:",
"@types/luxon": "catalog:",
"tailwindcss": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
@@ -403,7 +411,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "1.0.145",
"version": "1.0.153",
"dependencies": {
"zod": "catalog:",
},
@@ -414,7 +422,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.0.145",
"version": "1.0.153",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -470,7 +478,7 @@
"@tailwindcss/vite": "4.1.11",
"@tsconfig/bun": "1.0.9",
"@tsconfig/node22": "22.0.2",
"@types/bun": "1.3.3",
"@types/bun": "1.3.4",
"@types/luxon": "3.7.1",
"@types/node": "22.13.9",
"@typescript/native-preview": "7.0.0-dev.20251207.1",
@@ -1148,21 +1156,21 @@
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@opentui/core": ["@opentui/core@0.1.60", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.60", "@opentui/core-darwin-x64": "0.1.60", "@opentui/core-linux-arm64": "0.1.60", "@opentui/core-linux-x64": "0.1.60", "@opentui/core-win32-arm64": "0.1.60", "@opentui/core-win32-x64": "0.1.60", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-28jphd0AJo48uvEuKXcT9pJhgAu8I2rEJhPt25cc5ipJ2iw/eDk1uoxrbID80MPDqgOEzN21vXmzXwCd6ao+hg=="],
"@opentui/core": ["@opentui/core@0.0.0-20251211-4403a69a", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.0.0-20251211-4403a69a", "@opentui/core-darwin-x64": "0.0.0-20251211-4403a69a", "@opentui/core-linux-arm64": "0.0.0-20251211-4403a69a", "@opentui/core-linux-x64": "0.0.0-20251211-4403a69a", "@opentui/core-win32-arm64": "0.0.0-20251211-4403a69a", "@opentui/core-win32-x64": "0.0.0-20251211-4403a69a", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-wTZKcokyU9yiDqyC0Pvf9eRSdT73s4Ynerkit/z8Af++tynqrTlZHZCXK3o42Ff7itCSILmijcTU94n69aEypA=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.60", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N4feqnOBDA4O4yocpat5vOiV06HqJVwJGx8rEZE9DiOtl1i+1cPQ1Lx6+zWdLhbrVBJ0ENhb7Azox8sXkm/+5Q=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.0.0-20251211-4403a69a", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VAYjTa+Eiauy8gETXadD8y0PE6ppnKasDK1X354VoexZiWFR3r7rkL+TfDfk7whhqXDYyT44JDT1QmCAhVXRzQ=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.60", "", { "os": "darwin", "cpu": "x64" }, "sha512-+z3q4WaoIs7ANU8+eTFlvnfCjAS81rk81TOdZm4TJ53Ti3/B+yheWtnV/mLpLLhvZDz2VUVxxRmfDrGMnJb4fQ=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.0.0-20251211-4403a69a", "", { "os": "darwin", "cpu": "x64" }, "sha512-n9oVMpsojlILj1soORZzZ2Mjh8Zl73ZNcY7ot0iRmOjBDccrjDTsqKfxoGjKNd/xJSphLeu1LYGlcI5O5OczWQ=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.60", "", { "os": "linux", "cpu": "arm64" }, "sha512-/Q65sjqVGB9ygJ6lStI8n1X6RyfmJZC8XofRGEuFiMLiWcWC/xoBtztdL8LAIvHQy42y2+pl9zIiW0fWSQ0wjw=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.0.0-20251211-4403a69a", "", { "os": "linux", "cpu": "arm64" }, "sha512-vf4eUjPMI4ANitK4MpTGenZFddKgQD/K21aN6cZjusnH3mTEJAoIR7GbNtMdz3qclU43ajpzTID9sAwhshwdVQ=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.60", "", { "os": "linux", "cpu": "x64" }, "sha512-AegF+g7OguIpjZKN+PS55sc3ZFY6fj+fLwfETbSRGw6NqX+aiwpae0Y3gXX1s298Yq5yQEzMXnARTCJTGH4uzg=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.0.0-20251211-4403a69a", "", { "os": "linux", "cpu": "x64" }, "sha512-61635Up0YvVJ8gZ2eMiL1c8OfA+U6wAzT++LoaurNjbmsUAlKHws6MZdqTLw7aspJJVGsRFbA6d1Y+gXFxbDrQ=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.60", "", { "os": "win32", "cpu": "arm64" }, "sha512-fbkq8MOZJgT3r9q3JWqsfVxRpQ1SlbmhmvB35BzukXnZBK8eA178wbSadGH6irMDrkSIYye9WYddHI/iXjmgVQ=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.0.0-20251211-4403a69a", "", { "os": "win32", "cpu": "arm64" }, "sha512-3lUddTJGKZ6uU388eU79MY//IEbgGENCITetDrrRp7v9L1AxMntE1ihf6HniziwBvKKJcsUfqLiJWcq0WPZw2w=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.60", "", { "os": "win32", "cpu": "x64" }, "sha512-OebCL7f9+CKodBw0G+NvKIcc74bl6/sBEHfb73cACdJDJKh+T3C3Vt9H3kQQ0m1C8wRAqX6rh706OArk1pUb2A=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.0.0-20251211-4403a69a", "", { "os": "win32", "cpu": "x64" }, "sha512-Xwc1gqYsn8UZNTzNKkigZozAhBNBGbfX2B/I/aSbyqL0h8+XIInOodI0urzJWc0B6aEv/IDiT6Rm3coXFikLIg=="],
"@opentui/solid": ["@opentui/solid@0.1.60", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.60", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-pn91stzAHNGWaNL6h39q55bq3G1/DLqxKtT3wVsRAV68dHfPpwmqikX1nEJZK8OU84ZTPS9Ly9fz8po2Mot2uQ=="],
"@opentui/solid": ["@opentui/solid@0.0.0-20251211-4403a69a", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.0.0-20251211-4403a69a", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-vuLppAdd1Qgaqhie3q2TuEr+8udjT4d8uVg5arvCe1AUDVs19I8kvadVCfzGUVmtXgFIOEakbiv6AxDq5v9Zig=="],
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
@@ -1546,6 +1554,10 @@
"@solid-primitives/active-element": ["@solid-primitives/active-element@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9t5K4aR2naVDj950XU8OjnLgOg94a8k5wr6JNOPK+N5ESLsJDq42c1ZP8UKpewi1R+wplMMxiM6OPKRzbxJY7A=="],
"@solid-primitives/audio": ["@solid-primitives/audio@1.4.2", "", { "dependencies": { "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-UMD3ORQfI5Ky8yuKPxidDiEazsjv/dsoiKK5yZxLnsgaeNR1Aym3/77h/qT1jBYeXUgj4DX6t7NMpFUSVr14OQ=="],
"@solid-primitives/bounds": ["@solid-primitives/bounds@0.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/resize-observer": "^2.1.3", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-UbiyKMdSPmtijcEDnYLQL3zzaejpwWDAJJ4Gt5P0hgVs6A72piov0GyNw7V2SroH7NZFwxlYS22YmOr8A5xc1Q=="],
"@solid-primitives/event-bus": ["@solid-primitives/event-bus@1.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-l+n10/51neGcMaP3ypYt21bXfoeWh8IaC8k7fYuY3ww2a8S1Zv2N2a7FF5Qn+waTu86l0V8/nRHjkyqVIZBYwA=="],
"@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="],
@@ -1658,12 +1670,18 @@
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew=="],
"@tauri-apps/plugin-os": ["@tauri-apps/plugin-os@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A=="],
"@tauri-apps/plugin-process": ["@tauri-apps/plugin-process@2.3.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA=="],
"@tauri-apps/plugin-shell": ["@tauri-apps/plugin-shell@2.3.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Xod+pRcFxmOWFWEnqH5yZcA7qwAMuaaDkMR1Sply+F8VfBj++CGnj2xf5UoialmjZ2Cvd8qrvSCbU+7GgNVsKQ=="],
"@tauri-apps/plugin-store": ["@tauri-apps/plugin-store@2.4.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ckGSEzZ5Ii4Hf2D5x25Oqnm2Zf9MfDWAzR+volY0z/OOBz6aucPKEY0F649JvQ0Vupku6UJo7ugpGRDOFOunkA=="],
"@tauri-apps/plugin-updater": ["@tauri-apps/plugin-updater@2.9.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg=="],
"@tauri-apps/plugin-window-state": ["@tauri-apps/plugin-window-state@2.4.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-OuvdrzyY8Q5Dbzpj+GcrnV1iCeoZbcFdzMjanZMMcAEUNy/6PH5pxZPXpaZLOR7whlzXiuzx0L9EKZbH7zpdRw=="],
"@thisbeyond/solid-dnd": ["@thisbeyond/solid-dnd@0.7.5", "", { "peerDependencies": { "solid-js": "^1.5" } }, "sha512-DfI5ff+yYGpK9M21LhYwIPlbP2msKxN2ARwuu6GF8tT1GgNVDTI8VCQvH4TJFoVApP9d44izmAcTh/iTCH2UUw=="],
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
@@ -1686,7 +1704,7 @@
"@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
"@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
"@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="],
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
@@ -1992,7 +2010,7 @@
"bun-pty": ["bun-pty@0.4.2", "", {}, "sha512-sHImDz6pJDsHAroYpC9ouKVgOyqZ7FP3N+stX5IdMddHve3rf9LIZBDomQcXrACQ7sQDNuwZQHG8BKR7w8krkQ=="],
"bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
"bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
"bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="],
@@ -4278,6 +4296,10 @@
"openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="],
"opentui-spinner/@opentui/core": ["@opentui/core@0.1.60", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.60", "@opentui/core-darwin-x64": "0.1.60", "@opentui/core-linux-arm64": "0.1.60", "@opentui/core-linux-x64": "0.1.60", "@opentui/core-win32-arm64": "0.1.60", "@opentui/core-win32-x64": "0.1.60", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-28jphd0AJo48uvEuKXcT9pJhgAu8I2rEJhPt25cc5ipJ2iw/eDk1uoxrbID80MPDqgOEzN21vXmzXwCd6ao+hg=="],
"opentui-spinner/@opentui/solid": ["@opentui/solid@0.1.60", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.60", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-pn91stzAHNGWaNL6h39q55bq3G1/DLqxKtT3wVsRAV68dHfPpwmqikX1nEJZK8OU84ZTPS9Ly9fz8po2Mot2uQ=="],
"p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
"parse-bmfont-xml/xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="],
@@ -4854,6 +4876,22 @@
"opencontrol/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
"opentui-spinner/@opentui/core/@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.60", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N4feqnOBDA4O4yocpat5vOiV06HqJVwJGx8rEZE9DiOtl1i+1cPQ1Lx6+zWdLhbrVBJ0ENhb7Azox8sXkm/+5Q=="],
"opentui-spinner/@opentui/core/@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.60", "", { "os": "darwin", "cpu": "x64" }, "sha512-+z3q4WaoIs7ANU8+eTFlvnfCjAS81rk81TOdZm4TJ53Ti3/B+yheWtnV/mLpLLhvZDz2VUVxxRmfDrGMnJb4fQ=="],
"opentui-spinner/@opentui/core/@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.60", "", { "os": "linux", "cpu": "arm64" }, "sha512-/Q65sjqVGB9ygJ6lStI8n1X6RyfmJZC8XofRGEuFiMLiWcWC/xoBtztdL8LAIvHQy42y2+pl9zIiW0fWSQ0wjw=="],
"opentui-spinner/@opentui/core/@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.60", "", { "os": "linux", "cpu": "x64" }, "sha512-AegF+g7OguIpjZKN+PS55sc3ZFY6fj+fLwfETbSRGw6NqX+aiwpae0Y3gXX1s298Yq5yQEzMXnARTCJTGH4uzg=="],
"opentui-spinner/@opentui/core/@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.60", "", { "os": "win32", "cpu": "arm64" }, "sha512-fbkq8MOZJgT3r9q3JWqsfVxRpQ1SlbmhmvB35BzukXnZBK8eA178wbSadGH6irMDrkSIYye9WYddHI/iXjmgVQ=="],
"opentui-spinner/@opentui/core/@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.60", "", { "os": "win32", "cpu": "x64" }, "sha512-OebCL7f9+CKodBw0G+NvKIcc74bl6/sBEHfb73cACdJDJKh+T3C3Vt9H3kQQ0m1C8wRAqX6rh706OArk1pUb2A=="],
"opentui-spinner/@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
"opentui-spinner/@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.9", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.1" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.8" }, "optionalPeers": ["solid-js"] }, "sha512-pCnxWrciluXCeli/dj5PIEHgbNzim3evtTn12snjqqg8QZWJNMjH1AWIp4iG/tbVjqQ72aBEymMSagvmgxubXw=="],
"parse-bmfont-xml/xml2js/sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="],
"pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="],
@@ -5032,6 +5070,8 @@
"opencontrol/@modelcontextprotocol/sdk/raw-body/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"opentui-spinner/@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
"pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="],
Generated
+3 -3
View File
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1765270179,
"narHash": "sha256-g2a4MhRKu4ymR4xwo+I+auTknXt/+j37Lnf0Mvfl1rE=",
"lastModified": 1765425892,
"narHash": "sha256-jlQpSkg2sK6IJVzTQBDyRxQZgKADC2HKMRfGCSgNMHo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "677fbe97984e7af3175b6c121f3c39ee5c8d62c9",
"rev": "5d6bdbddb4695a62f0d00a3620b37a15275a5093",
"type": "github"
},
"original": {
+25
View File
@@ -17,13 +17,37 @@ inputs:
description: "Custom prompt to override the default prompt"
required: false
use_github_token:
description: "Use GITHUB_TOKEN directly instead of OpenCode App token exchange. When true, skips OIDC and uses the GITHUB_TOKEN env var."
required: false
default: "false"
runs:
using: "composite"
steps:
- name: Get opencode version
id: version
shell: bash
run: |
VERSION=$(curl -sf https://api.github.com/repos/sst/opencode/releases/latest | grep -o '"tag_name": *"[^"]*"' | cut -d'"' -f4)
echo "version=${VERSION:-latest}" >> $GITHUB_OUTPUT
- name: Cache opencode
id: cache
uses: actions/cache@v4
with:
path: ~/.opencode/bin
key: opencode-${{ runner.os }}-${{ runner.arch }}-${{ steps.version.outputs.version }}
- name: Install opencode
if: steps.cache.outputs.cache-hit != 'true'
shell: bash
run: curl -fsSL https://opencode.ai/install | bash
- name: Add opencode to PATH
shell: bash
run: echo "$HOME/.opencode/bin" >> $GITHUB_PATH
- name: Run opencode
shell: bash
id: run_opencode
@@ -32,3 +56,4 @@ runs:
MODEL: ${{ inputs.model }}
SHARE: ${{ inputs.share }}
PROMPT: ${{ inputs.prompt }}
USE_GITHUB_TOKEN: ${{ inputs.use_github_token }}
+1
View File
@@ -102,6 +102,7 @@ const ZEN_MODELS = [
new sst.Secret("ZEN_MODELS2"),
new sst.Secret("ZEN_MODELS3"),
new sst.Secret("ZEN_MODELS4"),
new sst.Secret("ZEN_MODELS5"),
]
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
+1 -1
View File
@@ -1,3 +1,3 @@
{
"nodeModules": "sha256-WcFRAG8w1XwgZxzyAawkQ7mYfJxu5VhA3sPApEG6FdI="
"nodeModules": "sha256-lgPsYtNJT7a+mDk5cTiEJLlBnTMTjxZCl8bw5WxcuaM="
}
+2 -2
View File
@@ -4,7 +4,7 @@
"description": "AI-powered development tool",
"private": true,
"type": "module",
"packageManager": "bun@1.3.3",
"packageManager": "bun@1.3.4",
"scripts": {
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
"typecheck": "bun turbo typecheck",
@@ -20,7 +20,7 @@
"packages/slack"
],
"catalog": {
"@types/bun": "1.3.3",
"@types/bun": "1.3.4",
"@hono/zod-validator": "0.4.2",
"ulid": "3.0.1",
"@kobalte/core": "0.13.11",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.0.145",
"version": "1.0.153",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+2
View File
@@ -3,6 +3,7 @@ import { Router } from "@solidjs/router"
import { FileRoutes } from "@solidjs/start/router"
import { Suspense } from "solid-js"
import { Favicon } from "@opencode-ai/ui/favicon"
import { Font } from "@opencode-ai/ui/font"
import "@ibm/plex/css/ibm-plex.css"
import "./app.css"
@@ -15,6 +16,7 @@ export default function App() {
<Title>opencode</Title>
<Meta name="description" content="OpenCode - The open source coding agent." />
<Favicon />
<Font />
<Suspense>{props.children}</Suspense>
</MetaProvider>
)}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

@@ -25,11 +25,8 @@ export function EmailSignup() {
const submission = useSubmission(emailSignup)
return (
<section data-component="email">
<div data-slot="dock">
<img src={dock} alt="" />
</div>
<div data-slot="section-title">
<h3>OpenCode will be available on desktop soon</h3>
<h3>Be the first to know when we release new products</h3>
<p>Join the waitlist for early access.</p>
</div>
<form data-slot="form" action={emailSignup} method="post">
@@ -34,7 +34,7 @@ const fetchSvgContent = async (svgPath: string): Promise<string> => {
}
}
export function Header(props: { zen?: boolean }) {
export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) {
const navigate = useNavigate()
const githubData = createAsync(() => github())
const starCount = createMemo(() =>
@@ -243,6 +243,13 @@ export function Header(props: { zen?: boolean }) {
</Match>
</Switch>
</li>
<Show when={!props.hideGetStarted}>
<li>
<A href="/download" data-slot="cta-button">
Get started for free
</A>
</li>
</Show>
</ul>
</nav>
</div>
+1
View File
@@ -26,6 +26,7 @@ export const github = query(async () => {
release: {
name: release.name,
url: release.html_url,
tag_name: release.tag_name,
},
contributors: contributorCount,
}
@@ -84,7 +84,16 @@
ul {
display: flex;
justify-content: space-between;
align-items: center;
gap: 48px;
@media (max-width: 55rem) {
gap: 32px;
}
@media (max-width: 48rem) {
gap: 24px;
}
li {
display: inline-block;
a {
@@ -98,6 +107,22 @@
text-underline-offset: 2px;
text-decoration-thickness: 1px;
}
[data-slot="cta-button"] {
background: var(--color-background-strong);
color: var(--color-text-inverted);
padding: 8px 16px;
border-radius: 4px;
font-weight: 500;
text-decoration: none;
@media (max-width: 55rem) {
display: none;
}
}
[data-slot="cta-button"]:hover {
background: var(--color-background-strong-hover);
text-decoration: none;
}
}
}
@@ -266,7 +291,7 @@
h1 {
font-size: 1.5rem;
font-weight: 500;
font-weight: 700;
color: var(--color-text-strong);
margin-bottom: 1rem;
}
@@ -0,0 +1,751 @@
::selection {
background: var(--color-background-interactive);
color: var(--color-text-strong);
@media (prefers-color-scheme: dark) {
background: var(--color-background-interactive);
color: var(--color-text-inverted);
}
}
[data-page="download"] {
--color-background: hsl(0, 20%, 99%);
--color-background-weak: hsl(0, 8%, 97%);
--color-background-weak-hover: hsl(0, 8%, 94%);
--color-background-strong: hsl(0, 5%, 12%);
--color-background-strong-hover: hsl(0, 5%, 18%);
--color-background-interactive: hsl(62, 84%, 88%);
--color-background-interactive-weaker: hsl(64, 74%, 95%);
--color-text: hsl(0, 1%, 39%);
--color-text-weak: hsl(0, 1%, 60%);
--color-text-weaker: hsl(30, 2%, 81%);
--color-text-strong: hsl(0, 5%, 12%);
--color-text-inverted: hsl(0, 20%, 99%);
--color-text-success: hsl(119, 100%, 35%);
--color-border: hsl(30, 2%, 81%);
--color-border-weak: hsl(0, 1%, 85%);
--color-icon: hsl(0, 1%, 55%);
--color-success: hsl(142, 76%, 36%);
background: var(--color-background);
font-family: var(--font-mono);
color: var(--color-text);
padding-bottom: 5rem;
overflow-x: hidden;
@media (prefers-color-scheme: dark) {
--color-background: hsl(0, 9%, 7%);
--color-background-weak: hsl(0, 6%, 10%);
--color-background-weak-hover: hsl(0, 6%, 15%);
--color-background-strong: hsl(0, 15%, 94%);
--color-background-strong-hover: hsl(0, 15%, 97%);
--color-background-interactive: hsl(62, 100%, 90%);
--color-background-interactive-weaker: hsl(60, 20%, 8%);
--color-text: hsl(0, 4%, 71%);
--color-text-weak: hsl(0, 2%, 49%);
--color-text-weaker: hsl(0, 3%, 28%);
--color-text-strong: hsl(0, 15%, 94%);
--color-text-inverted: hsl(0, 9%, 7%);
--color-text-success: hsl(119, 60%, 72%);
--color-border: hsl(0, 3%, 28%);
--color-border-weak: hsl(0, 4%, 23%);
--color-icon: hsl(10, 3%, 43%);
--color-success: hsl(142, 76%, 46%);
}
/* Header and Footer styles - copied from enterprise */
[data-component="top"] {
padding: 24px 5rem;
height: 80px;
position: sticky;
top: 0;
display: flex;
justify-content: space-between;
align-items: center;
background: var(--color-background);
border-bottom: 1px solid var(--color-border-weak);
z-index: 10;
@media (max-width: 60rem) {
padding: 24px 1.5rem;
}
img {
height: 34px;
width: auto;
}
[data-component="nav-desktop"] {
ul {
display: flex;
justify-content: space-between;
align-items: center;
gap: 48px;
@media (max-width: 55rem) {
gap: 32px;
}
@media (max-width: 48rem) {
gap: 24px;
}
li {
display: inline-block;
a {
text-decoration: none;
span {
color: var(--color-text-weak);
}
}
a:hover {
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
}
[data-slot="cta-button"] {
background: var(--color-background-strong);
color: var(--color-text-inverted);
padding: 8px 16px;
border-radius: 4px;
font-weight: 500;
text-decoration: none;
@media (max-width: 55rem) {
display: none;
}
}
[data-slot="cta-button"]:hover {
background: var(--color-background-strong-hover);
text-decoration: none;
}
}
}
@media (max-width: 40rem) {
display: none;
}
}
[data-component="nav-mobile"] {
button > svg {
color: var(--color-icon);
}
}
[data-component="nav-mobile-toggle"] {
border: none;
background: none;
outline: none;
height: 40px;
width: 40px;
cursor: pointer;
margin-right: -8px;
}
[data-component="nav-mobile-toggle"]:hover {
background: var(--color-background-weak);
}
[data-component="nav-mobile"] {
display: none;
@media (max-width: 40rem) {
display: block;
[data-component="nav-mobile-icon"] {
cursor: pointer;
height: 40px;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
}
[data-component="nav-mobile-menu-list"] {
position: fixed;
background: var(--color-background);
top: 80px;
left: 0;
right: 0;
height: 100vh;
ul {
list-style: none;
padding: 20px 0;
li {
a {
text-decoration: none;
padding: 20px;
display: block;
span {
color: var(--color-text-weak);
}
}
a:hover {
background: var(--color-background-weak);
}
}
}
}
}
}
[data-slot="logo dark"] {
display: none;
}
@media (prefers-color-scheme: dark) {
[data-slot="logo light"] {
display: none;
}
[data-slot="logo dark"] {
display: block;
}
}
}
[data-component="footer"] {
border-top: 1px solid var(--color-border-weak);
display: flex;
flex-direction: row;
@media (max-width: 65rem) {
border-bottom: 1px solid var(--color-border-weak);
}
[data-slot="cell"] {
flex: 1;
text-align: center;
a {
text-decoration: none;
padding: 2rem 0;
width: 100%;
display: block;
span {
color: var(--color-text-weak);
@media (max-width: 40rem) {
display: none;
}
}
}
a:hover {
background: var(--color-background-weak);
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
}
}
[data-slot="cell"] + [data-slot="cell"] {
border-left: 1px solid var(--color-border-weak);
@media (max-width: 40rem) {
border-left: none;
}
}
@media (max-width: 25rem) {
flex-wrap: wrap;
[data-slot="cell"] {
flex: 1 0 100%;
border-left: none;
border-top: 1px solid var(--color-border-weak);
}
[data-slot="cell"]:nth-child(1) {
border-top: none;
}
}
}
[data-component="container"] {
max-width: 67.5rem;
margin: 0 auto;
border: 1px solid var(--color-border-weak);
border-top: none;
@media (max-width: 65rem) {
border: none;
}
}
[data-component="content"] {
padding: 6rem 5rem;
@media (max-width: 60rem) {
padding: 4rem 1.5rem;
}
}
[data-component="legal"] {
color: var(--color-text-weak);
text-align: center;
padding: 2rem 5rem;
display: flex;
gap: 32px;
justify-content: center;
@media (max-width: 60rem) {
padding: 2rem 1.5rem;
}
a {
color: var(--color-text-weak);
text-decoration: none;
}
a:hover {
color: var(--color-text);
text-decoration: underline;
}
}
/* Download Hero Section */
[data-component="download-hero"] {
display: grid;
grid-template-columns: 260px 1fr;
gap: 4rem;
padding-bottom: 2rem;
margin-bottom: 4rem;
@media (max-width: 50rem) {
grid-template-columns: 1fr;
gap: 1.5rem;
padding-bottom: 2rem;
margin-bottom: 2rem;
}
[data-component="hero-icon"] {
display: flex;
justify-content: flex-end;
align-items: center;
@media (max-width: 40rem) {
display: none;
}
[data-slot="icon-placeholder"] {
width: 120px;
height: 120px;
background: var(--color-background-weak);
border: 1px solid var(--color-border-weak);
border-radius: 24px;
@media (max-width: 50rem) {
width: 80px;
height: 80px;
}
}
img {
width: 120px;
height: 120px;
border-radius: 24px;
box-shadow:
0 1.467px 2.847px 0 rgba(0, 0, 0, 0.42),
0 0.779px 1.512px 0 rgba(0, 0, 0, 0.34),
0 0.324px 0.629px 0 rgba(0, 0, 0, 0.24);
@media (max-width: 50rem) {
width: 80px;
height: 80px;
border-radius: 16px;
}
}
@media (max-width: 50rem) {
justify-content: flex-start;
}
}
[data-component="hero-text"] {
display: flex;
flex-direction: column;
justify-content: center;
h1 {
font-size: 1.5rem;
font-weight: 700;
color: var(--color-text-strong);
margin-bottom: 4px;
@media (max-width: 40rem) {
margin-bottom: 1rem;
}
}
p {
color: var(--color-text);
margin-bottom: 12px;
@media (max-width: 40rem) {
margin-bottom: 2.5rem;
line-height: 1.6;
}
}
[data-component="download-button"] {
padding: 8px 20px 8px 16px;
background: var(--color-background-strong);
color: var(--color-text-inverted);
border: none;
border-radius: 4px;
font-weight: 500;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 10px;
transition: all 0.2s ease;
text-decoration: none;
width: fit-content;
&:hover:not(:disabled) {
background: var(--color-background-strong-hover);
}
&:active {
transform: scale(0.98);
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
}
}
/* Download Sections */
[data-component="download-section"] {
display: grid;
grid-template-columns: 260px 1fr;
gap: 4rem;
margin-bottom: 4rem;
@media (max-width: 50rem) {
grid-template-columns: 1fr;
gap: 1rem;
margin-bottom: 3rem;
}
&:last-child {
margin-bottom: 0;
}
[data-component="section-label"] {
font-weight: 500;
color: var(--color-text-strong);
padding-top: 1rem;
span {
color: var(--color-text-weaker);
}
@media (max-width: 50rem) {
padding-top: 0;
padding-bottom: 0.5rem;
}
}
[data-component="section-content"] {
display: flex;
flex-direction: column;
gap: 0;
}
}
/* CLI Rows */
button[data-component="cli-row"] {
display: flex;
align-items: center;
gap: 12px;
padding: 1rem 0.5rem 1rem 1.5rem;
margin: 0 -0.5rem 0 -1.5rem;
background: none;
border: none;
border-radius: 4px;
width: calc(100% + 2rem);
text-align: left;
cursor: pointer;
transition: background 0.15s ease;
&:hover {
background: var(--color-background-weak);
}
code {
font-family: var(--font-mono);
color: var(--color-text-weak);
strong {
color: var(--color-text-strong);
font-weight: 500;
}
}
[data-component="copy-status"] {
display: flex;
align-items: center;
opacity: 0;
transition: opacity 0.15s ease;
color: var(--color-icon);
svg {
width: 18px;
height: 18px;
}
[data-slot="copy"] {
display: block;
}
[data-slot="check"] {
display: none;
}
}
&:hover [data-component="copy-status"] {
opacity: 1;
}
&[data-copied] [data-component="copy-status"] {
opacity: 1;
[data-slot="copy"] {
display: none;
}
[data-slot="check"] {
display: block;
}
}
}
/* Download Rows */
[data-component="download-row"] {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0.5rem 0.75rem 1.5rem;
margin: 0 -0.5rem 0 -1.5rem;
border-radius: 4px;
transition: background 0.15s ease;
&:hover {
background: var(--color-background-weak);
}
[data-component="download-info"] {
display: flex;
align-items: center;
gap: 0.75rem;
[data-slot="icon"] {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-icon);
svg {
width: 20px;
height: 20px;
}
img {
width: 20px;
height: 20px;
}
}
span {
color: var(--color-text);
}
}
[data-component="action-button"] {
padding: 6px 16px;
background: var(--color-background);
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: 4px;
font-weight: 500;
cursor: pointer;
text-decoration: none;
transition: all 0.2s ease;
&:hover {
background: var(--color-background-weak);
border-color: var(--color-border);
text-decoration: none;
}
&:active {
transform: scale(0.98);
}
}
}
a {
color: var(--color-text-strong);
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
&:hover {
text-decoration-thickness: 2px;
}
}
/* Narrow screen font sizes */
@media (max-width: 40rem) {
[data-component="download-section"] {
[data-component="section-label"] {
font-size: 14px;
}
}
button[data-component="cli-row"] {
margin: 0;
padding: 1rem 0;
width: 100%;
overflow: hidden;
code {
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
max-width: calc(100vw - 80px);
}
[data-component="copy-status"] {
opacity: 1 !important;
flex-shrink: 0;
}
}
[data-component="download-row"] {
margin: 0;
padding: 0.75rem 0;
[data-component="download-info"] span {
font-size: 14px;
}
[data-component="action-button"] {
font-size: 14px;
padding-left: 8px;
padding-right: 8px;
}
}
}
@media (max-width: 22.5rem) {
[data-slot="hide-narrow"] {
display: none;
}
}
/* FAQ Section */
[data-component="faq"] {
border-top: 1px solid var(--color-border-weak);
padding: 4rem 5rem;
margin-top: 4rem;
@media (max-width: 60rem) {
padding: 3rem 1.5rem;
margin-top: 3rem;
}
[data-slot="section-title"] {
margin-bottom: 24px;
h3 {
font-size: 16px;
font-weight: 700;
color: var(--color-text-strong);
margin-bottom: 12px;
}
}
ul {
padding: 0;
li {
list-style: none;
margin-bottom: 24px;
line-height: 200%;
}
}
[data-slot="faq-question"] {
display: flex;
gap: 16px;
margin-bottom: 8px;
color: var(--color-text-strong);
font-weight: 500;
cursor: pointer;
background: none;
border: none;
padding: 0;
align-items: start;
min-height: 24px;
svg {
margin-top: 2px;
}
[data-slot="faq-icon-plus"] {
flex-shrink: 0;
color: var(--color-text-weak);
margin-top: 2px;
[data-closed] & {
display: block;
}
[data-expanded] & {
display: none;
}
}
[data-slot="faq-icon-minus"] {
flex-shrink: 0;
color: var(--color-text-weak);
margin-top: 2px;
[data-closed] & {
display: none;
}
[data-expanded] & {
display: block;
}
}
[data-slot="faq-question-text"] {
flex-grow: 1;
text-align: left;
}
}
[data-slot="faq-answer"] {
margin-left: 40px;
margin-bottom: 32px;
line-height: 200%;
}
}
}
@@ -0,0 +1,410 @@
import "./index.css"
import { Title, Meta, Link } from "@solidjs/meta"
import { A, createAsync, query } from "@solidjs/router"
import { Header } from "~/component/header"
import { Footer } from "~/component/footer"
import { IconCopy, IconCheck } from "~/component/icon"
import { Faq } from "~/component/faq"
import desktopAppIcon from "../../asset/lander/opencode-desktop-icon.png"
import { Legal } from "~/component/legal"
import { config } from "~/config"
import { github } from "~/lib/github"
function CopyStatus() {
return (
<span data-component="copy-status">
<IconCopy data-slot="copy" />
<IconCheck data-slot="check" />
</span>
)
}
export default function Download() {
const githubData = createAsync(() => github(), {
deferStream: true,
})
const download = () => {
const version = githubData()?.release.tag_name
if (!version) return null
return `https://github.com/sst/opencode/releases/download/${version}`
}
const handleCopyClick = (command: string) => (event: Event) => {
const button = event.currentTarget as HTMLButtonElement
navigator.clipboard.writeText(command)
button.setAttribute("data-copied", "")
setTimeout(() => {
button.removeAttribute("data-copied")
}, 1500)
}
return (
<main data-page="download">
<Title>OpenCode | Download</Title>
<Link rel="canonical" href={`${config.baseUrl}/download`} />
<Meta name="description" content="Download OpenCode for macOS, Windows, and Linux" />
<div data-component="container">
<Header hideGetStarted />
<div data-component="content">
<section data-component="download-hero">
<div data-component="hero-icon">
<img src={desktopAppIcon} alt="OpenCode Desktop" />
</div>
<div data-component="hero-text">
<h1>Download OpenCode</h1>
<p>Available in Beta for macOS, Windows, and Linux</p>
</div>
</section>
<section data-component="download-section">
<div data-component="section-label">
<span>[1]</span> OpenCode Terminal
</div>
<div data-component="section-content">
<button
data-component="cli-row"
onClick={handleCopyClick("curl -fsSL https://opencode.ai/install | bash")}
>
<code>
curl -fsSL https://<strong>opencode.ai/install</strong> | bash
</code>
<CopyStatus />
</button>
<button data-component="cli-row" onClick={handleCopyClick("npm i -g opencode-ai")}>
<code>
npm i -g <strong>opencode-ai</strong>
</code>
<CopyStatus />
</button>
<button data-component="cli-row" onClick={handleCopyClick("bun add -g opencode-ai")}>
<code>
bun add -g <strong>opencode-ai</strong>
</code>
<CopyStatus />
</button>
<button data-component="cli-row" onClick={handleCopyClick("brew install opencode")}>
<code>
brew install <strong>opencode</strong>
</code>
<CopyStatus />
</button>
<button data-component="cli-row" onClick={handleCopyClick("paru -S opencode")}>
<code>
paru -S <strong>opencode</strong>
</code>
<CopyStatus />
</button>
</div>
</section>
<section data-component="download-section">
<div data-component="section-label">
<span>[2]</span> OpenCode Desktop (Beta)
</div>
<div data-component="section-content">
<div data-component="download-row">
<div data-component="download-info">
<span data-slot="icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M20.0035 7.15814C19.3171 7.5784 18.7485 8.16594 18.351 8.86579C17.9534 9.56563 17.74 10.3549 17.7305 11.1597C17.7332 12.0655 18.0016 12.9506 18.5024 13.7054C19.0032 14.4602 19.7144 15.0515 20.5479 15.4061C20.2193 16.4664 19.7329 17.4712 19.1051 18.3868C18.2069 19.6798 17.2677 20.9727 15.8387 20.9727C14.4096 20.9727 14.0421 20.1425 12.3952 20.1425C10.7892 20.1425 10.2175 21 8.91088 21C7.60426 21 6.69246 19.8022 5.6444 18.3323C4.25999 16.2732 3.49913 13.8583 3.45312 11.3774C3.45312 7.29427 6.10722 5.13028 8.72032 5.13028C10.1086 5.13028 11.2656 6.04208 12.1366 6.04208C12.9669 6.04208 14.2599 5.07572 15.8387 5.07572C16.6504 5.05478 17.4548 5.23375 18.1811 5.59689C18.9074 5.96003 19.5332 6.49619 20.0035 7.15814ZM15.0901 3.34726C15.7861 2.52858 16.18 1.49589 16.2062 0.421702C16.2074 0.280092 16.1937 0.13875 16.1654 0C14.9699 0.116777 13.8644 0.686551 13.0757 1.59245C12.3731 2.37851 11.9643 3.38362 11.9188 4.43697C11.9193 4.56507 11.933 4.69278 11.9597 4.81808C12.0539 4.8359 12.1496 4.84503 12.2455 4.84536C12.7964 4.80152 13.3327 4.64611 13.8217 4.38858C14.3108 4.13104 14.7423 3.77676 15.0901 3.34726Z"
fill="currentColor"
/>
</svg>
</span>
<span>
macOS (<span data-slot="hide-narrow">Apple </span>Silicon)
</span>
</div>
<a href={download() + "/opencode-desktop-darwin-aarch64.dmg"} data-component="action-button">
Download
</a>
</div>
<div data-component="download-row">
<div data-component="download-info">
<span data-slot="icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M20.0035 7.15814C19.3171 7.5784 18.7485 8.16594 18.351 8.86579C17.9534 9.56563 17.74 10.3549 17.7305 11.1597C17.7332 12.0655 18.0016 12.9506 18.5024 13.7054C19.0032 14.4602 19.7144 15.0515 20.5479 15.4061C20.2193 16.4664 19.7329 17.4712 19.1051 18.3868C18.2069 19.6798 17.2677 20.9727 15.8387 20.9727C14.4096 20.9727 14.0421 20.1425 12.3952 20.1425C10.7892 20.1425 10.2175 21 8.91088 21C7.60426 21 6.69246 19.8022 5.6444 18.3323C4.25999 16.2732 3.49913 13.8583 3.45312 11.3774C3.45312 7.29427 6.10722 5.13028 8.72032 5.13028C10.1086 5.13028 11.2656 6.04208 12.1366 6.04208C12.9669 6.04208 14.2599 5.07572 15.8387 5.07572C16.6504 5.05478 17.4548 5.23375 18.1811 5.59689C18.9074 5.96003 19.5332 6.49619 20.0035 7.15814ZM15.0901 3.34726C15.7861 2.52858 16.18 1.49589 16.2062 0.421702C16.2074 0.280092 16.1937 0.13875 16.1654 0C14.9699 0.116777 13.8644 0.686551 13.0757 1.59245C12.3731 2.37851 11.9643 3.38362 11.9188 4.43697C11.9193 4.56507 11.933 4.69278 11.9597 4.81808C12.0539 4.8359 12.1496 4.84503 12.2455 4.84536C12.7964 4.80152 13.3327 4.64611 13.8217 4.38858C14.3108 4.13104 14.7423 3.77676 15.0901 3.34726Z"
fill="currentColor"
/>
</svg>
</span>
<span>macOS (Intel)</span>
</div>
<a href={download() + "/opencode-desktop-darwin-x64.dmg"} data-component="action-button">
Download
</a>
</div>
<div data-component="download-row">
<div data-component="download-info">
<span data-slot="icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2614_159729)">
<path
d="M2 2H11.481V11.4769H2V2ZM12.519 2H22V11.4769H12.519V2ZM2 12.519H11.481V22H2V12.519ZM12.519 12.519H22V22H12.519"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="clip0_2614_159729">
<rect width="20" height="20" fill="white" transform="translate(2 2)" />
</clipPath>
</defs>
</svg>
</span>
<span>Windows (x64)</span>
</div>
<a href={download() + "/opencode-desktop-windows-x64.exe"} data-component="action-button">
Download
</a>
</div>
<div data-component="download-row">
<div data-component="download-info">
<span data-slot="icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M4.34591 22.7088C5.61167 22.86 7.03384 23.6799 8.22401 23.8247C9.42058 23.9758 9.79086 23.0098 9.79086 23.0098C9.79086 23.0098 11.1374 22.7088 12.553 22.6741C13.97 22.6344 15.3113 22.9688 15.3113 22.9688C15.3113 22.9688 15.5714 23.5646 16.057 23.8247C16.5426 24.0898 17.588 24.1257 18.258 23.4198C18.9293 22.7088 20.7204 21.8132 21.7261 21.2533C22.7382 20.6922 22.5525 19.8364 21.917 19.5763C21.2816 19.3163 20.7614 18.9063 20.8011 18.1196C20.8357 17.3394 20.24 16.8193 20.24 16.8193C20.24 16.8193 20.7614 15.1025 20.2759 13.6805C19.7903 12.2648 18.1889 9.98819 16.9577 8.27657C15.7266 6.55985 16.7719 4.5779 15.651 2.04503C14.5299 -0.491656 11.623 -0.341713 10.0562 0.739505C8.4893 1.8208 8.96968 4.50225 9.04526 5.77447C9.12084 7.04022 9.07985 7.94598 8.93509 8.27146C8.79033 8.60198 7.77951 9.80243 7.1082 10.8081C6.43818 11.819 5.95254 13.906 5.46187 14.7669C4.98142 15.6228 5.31711 16.403 5.31711 16.403C5.31711 16.403 4.98149 16.5182 4.71628 17.0795C4.45616 17.6342 3.93601 17.8993 2.99948 18.0801C2.06934 18.2709 2.06934 18.8705 2.29357 19.5419C2.51902 20.2119 2.29357 20.5873 2.03346 21.4431C1.77342 22.2988 3.07506 22.5588 4.34591 22.7088ZM17.5034 18.805C18.1683 19.0958 19.124 18.691 19.4149 18.4001C19.7045 18.1106 19.9094 17.6801 19.9094 17.6801C19.9094 17.6801 20.2002 17.8249 20.1707 18.2848C20.14 18.7512 20.3706 19.4161 20.8062 19.6467C21.2418 19.876 21.9067 20.1963 21.5621 20.5166C21.211 20.8369 19.2688 21.6183 18.6885 22.2282C18.1132 22.8341 17.3573 23.33 16.8974 23.1839C16.4324 23.0391 16.0262 22.4037 16.2261 21.4736C16.4324 20.5473 16.6066 19.5313 16.5771 18.951C16.5464 18.3707 16.4324 17.5892 16.5771 17.4738C16.7219 17.3598 16.9525 17.4148 16.9525 17.4148C16.9525 17.4148 16.8371 18.5156 17.5034 18.805ZM13.1885 3.12632C13.829 3.12632 14.3454 3.76175 14.3454 4.54324C14.3454 5.09798 14.0853 5.57844 13.7048 5.80906C13.6087 5.76937 13.5087 5.72449 13.3986 5.67832C13.6292 5.56434 13.7893 5.27352 13.7893 4.93783C13.7893 4.49844 13.519 4.13714 13.1794 4.13714C12.8489 4.13714 12.5734 4.49836 12.5734 4.93783C12.5734 5.09806 12.6132 5.25813 12.6785 5.38369C12.4786 5.30293 12.298 5.23383 12.1532 5.17874C12.0776 4.98781 12.0328 4.77257 12.0328 4.54331C12.0328 3.76183 12.5478 3.12632 13.1885 3.12632ZM11.6024 5.56823C11.9176 5.62331 12.7835 5.9987 13.1039 6.11398C13.4242 6.22415 13.7791 6.4291 13.7445 6.63413C13.7048 6.84548 13.5395 6.84548 13.1039 7.1107C12.6735 7.37082 11.7331 7.95116 11.432 7.99085C11.1322 8.03055 10.9618 7.86141 10.6415 7.65516C10.3211 7.44503 9.72039 6.95436 9.87147 6.69432C9.87147 6.69432 10.3416 6.33432 10.5467 6.14986C10.7517 5.95893 11.2821 5.50925 11.6024 5.56823ZM10.2213 3.35185C10.726 3.35185 11.1373 3.95268 11.1373 4.69318C11.1373 4.82773 11.1219 4.95322 11.0976 5.07878C10.972 5.11847 10.8466 5.18385 10.726 5.28891C10.6671 5.33889 10.612 5.38369 10.5621 5.43367C10.6415 5.28381 10.6722 5.06857 10.6363 4.84305C10.5672 4.44335 10.2968 4.14743 10.0316 4.18712C9.76511 4.232 9.60625 4.5984 9.67033 5.00327C9.74081 5.41325 10.0059 5.7091 10.2763 5.6643C10.2917 5.6592 10.3058 5.65409 10.3211 5.64891C10.1918 5.77447 10.0713 5.88464 9.94576 5.97432C9.58065 5.80388 9.31033 5.29402 9.31033 4.69318C9.31041 3.94758 9.71521 3.35185 10.2213 3.35185ZM7.40915 13.045C7.9293 12.2251 8.26492 10.4328 8.78507 9.83702C9.31041 9.24259 9.71521 7.97554 9.53075 7.41569C9.53075 7.41569 10.6517 8.75702 11.432 8.53668C12.2135 8.31116 13.97 7.00571 14.23 7.22994C14.4901 7.45539 16.727 12.375 16.9525 13.9419C17.178 15.5074 16.8026 16.7041 16.8026 16.7041C16.8026 16.7041 15.9468 16.4785 15.8366 16.9987C15.7264 17.524 15.7264 19.4265 15.7264 19.4265C15.7264 19.4265 14.5695 21.0279 12.7784 21.2931C10.9874 21.5532 10.0905 21.3636 10.0905 21.3636L9.08481 20.2118C9.08481 20.2118 9.86637 20.0965 9.75612 19.3112C9.64595 18.531 7.36801 17.4496 6.95803 16.4785C6.5482 15.5073 6.8826 13.8662 7.40915 13.045ZM2.9802 18.9204C3.06988 18.5361 4.23056 18.5361 4.67643 18.2657C5.12229 17.9954 5.21189 17.219 5.57197 17.0141C5.92679 16.804 6.58279 17.5496 6.85311 17.9697C7.11833 18.3797 8.13433 20.1721 8.54942 20.6179C8.96961 21.0676 9.35528 21.6633 9.23483 22.1988C9.12084 22.7343 8.48923 23.1251 8.48923 23.1251C7.92427 23.2993 6.34843 22.619 5.63231 22.3192C4.9162 22.0182 3.09433 21.9284 2.8599 21.6633C2.61906 21.393 2.97517 20.7972 3.06995 20.2322C3.15445 19.6609 2.8893 19.306 2.9802 18.9204Z"
fill="currentColor"
/>
</svg>
</span>
<span>Linux (.deb)</span>
</div>
<a href={download() + "/opencode-desktop-linux-amd64.deb"} data-component="action-button">
Download
</a>
</div>
<div data-component="download-row">
<div data-component="download-info">
<span data-slot="icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M4.34591 22.7088C5.61167 22.86 7.03384 23.6799 8.22401 23.8247C9.42058 23.9758 9.79086 23.0098 9.79086 23.0098C9.79086 23.0098 11.1374 22.7088 12.553 22.6741C13.97 22.6344 15.3113 22.9688 15.3113 22.9688C15.3113 22.9688 15.5714 23.5646 16.057 23.8247C16.5426 24.0898 17.588 24.1257 18.258 23.4198C18.9293 22.7088 20.7204 21.8132 21.7261 21.2533C22.7382 20.6922 22.5525 19.8364 21.917 19.5763C21.2816 19.3163 20.7614 18.9063 20.8011 18.1196C20.8357 17.3394 20.24 16.8193 20.24 16.8193C20.24 16.8193 20.7614 15.1025 20.2759 13.6805C19.7903 12.2648 18.1889 9.98819 16.9577 8.27657C15.7266 6.55985 16.7719 4.5779 15.651 2.04503C14.5299 -0.491656 11.623 -0.341713 10.0562 0.739505C8.4893 1.8208 8.96968 4.50225 9.04526 5.77447C9.12084 7.04022 9.07985 7.94598 8.93509 8.27146C8.79033 8.60198 7.77951 9.80243 7.1082 10.8081C6.43818 11.819 5.95254 13.906 5.46187 14.7669C4.98142 15.6228 5.31711 16.403 5.31711 16.403C5.31711 16.403 4.98149 16.5182 4.71628 17.0795C4.45616 17.6342 3.93601 17.8993 2.99948 18.0801C2.06934 18.2709 2.06934 18.8705 2.29357 19.5419C2.51902 20.2119 2.29357 20.5873 2.03346 21.4431C1.77342 22.2988 3.07506 22.5588 4.34591 22.7088ZM17.5034 18.805C18.1683 19.0958 19.124 18.691 19.4149 18.4001C19.7045 18.1106 19.9094 17.6801 19.9094 17.6801C19.9094 17.6801 20.2002 17.8249 20.1707 18.2848C20.14 18.7512 20.3706 19.4161 20.8062 19.6467C21.2418 19.876 21.9067 20.1963 21.5621 20.5166C21.211 20.8369 19.2688 21.6183 18.6885 22.2282C18.1132 22.8341 17.3573 23.33 16.8974 23.1839C16.4324 23.0391 16.0262 22.4037 16.2261 21.4736C16.4324 20.5473 16.6066 19.5313 16.5771 18.951C16.5464 18.3707 16.4324 17.5892 16.5771 17.4738C16.7219 17.3598 16.9525 17.4148 16.9525 17.4148C16.9525 17.4148 16.8371 18.5156 17.5034 18.805ZM13.1885 3.12632C13.829 3.12632 14.3454 3.76175 14.3454 4.54324C14.3454 5.09798 14.0853 5.57844 13.7048 5.80906C13.6087 5.76937 13.5087 5.72449 13.3986 5.67832C13.6292 5.56434 13.7893 5.27352 13.7893 4.93783C13.7893 4.49844 13.519 4.13714 13.1794 4.13714C12.8489 4.13714 12.5734 4.49836 12.5734 4.93783C12.5734 5.09806 12.6132 5.25813 12.6785 5.38369C12.4786 5.30293 12.298 5.23383 12.1532 5.17874C12.0776 4.98781 12.0328 4.77257 12.0328 4.54331C12.0328 3.76183 12.5478 3.12632 13.1885 3.12632ZM11.6024 5.56823C11.9176 5.62331 12.7835 5.9987 13.1039 6.11398C13.4242 6.22415 13.7791 6.4291 13.7445 6.63413C13.7048 6.84548 13.5395 6.84548 13.1039 7.1107C12.6735 7.37082 11.7331 7.95116 11.432 7.99085C11.1322 8.03055 10.9618 7.86141 10.6415 7.65516C10.3211 7.44503 9.72039 6.95436 9.87147 6.69432C9.87147 6.69432 10.3416 6.33432 10.5467 6.14986C10.7517 5.95893 11.2821 5.50925 11.6024 5.56823ZM10.2213 3.35185C10.726 3.35185 11.1373 3.95268 11.1373 4.69318C11.1373 4.82773 11.1219 4.95322 11.0976 5.07878C10.972 5.11847 10.8466 5.18385 10.726 5.28891C10.6671 5.33889 10.612 5.38369 10.5621 5.43367C10.6415 5.28381 10.6722 5.06857 10.6363 4.84305C10.5672 4.44335 10.2968 4.14743 10.0316 4.18712C9.76511 4.232 9.60625 4.5984 9.67033 5.00327C9.74081 5.41325 10.0059 5.7091 10.2763 5.6643C10.2917 5.6592 10.3058 5.65409 10.3211 5.64891C10.1918 5.77447 10.0713 5.88464 9.94576 5.97432C9.58065 5.80388 9.31033 5.29402 9.31033 4.69318C9.31041 3.94758 9.71521 3.35185 10.2213 3.35185ZM7.40915 13.045C7.9293 12.2251 8.26492 10.4328 8.78507 9.83702C9.31041 9.24259 9.71521 7.97554 9.53075 7.41569C9.53075 7.41569 10.6517 8.75702 11.432 8.53668C12.2135 8.31116 13.97 7.00571 14.23 7.22994C14.4901 7.45539 16.727 12.375 16.9525 13.9419C17.178 15.5074 16.8026 16.7041 16.8026 16.7041C16.8026 16.7041 15.9468 16.4785 15.8366 16.9987C15.7264 17.524 15.7264 19.4265 15.7264 19.4265C15.7264 19.4265 14.5695 21.0279 12.7784 21.2931C10.9874 21.5532 10.0905 21.3636 10.0905 21.3636L9.08481 20.2118C9.08481 20.2118 9.86637 20.0965 9.75612 19.3112C9.64595 18.531 7.36801 17.4496 6.95803 16.4785C6.5482 15.5073 6.8826 13.8662 7.40915 13.045ZM2.9802 18.9204C3.06988 18.5361 4.23056 18.5361 4.67643 18.2657C5.12229 17.9954 5.21189 17.219 5.57197 17.0141C5.92679 16.804 6.58279 17.5496 6.85311 17.9697C7.11833 18.3797 8.13433 20.1721 8.54942 20.6179C8.96961 21.0676 9.35528 21.6633 9.23483 22.1988C9.12084 22.7343 8.48923 23.1251 8.48923 23.1251C7.92427 23.2993 6.34843 22.619 5.63231 22.3192C4.9162 22.0182 3.09433 21.9284 2.8599 21.6633C2.61906 21.393 2.97517 20.7972 3.06995 20.2322C3.15445 19.6609 2.8893 19.306 2.9802 18.9204Z"
fill="currentColor"
/>
</svg>
</span>
<span>Linux (.rpm)</span>
</div>
<a href={download() + "/opencode-desktop-linux-x86_64.rpm"} data-component="action-button">
Download
</a>
</div>
</div>
</section>
<section data-component="download-section">
<div data-component="section-label">
<span>[3]</span> OpenCode Extensions
</div>
<div data-component="section-content">
<div data-component="download-row">
<div data-component="download-info">
<span data-slot="icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2614_159777)">
<path
d="M21.7899 4.15451L17.6755 2.17514C17.1968 1.94389 16.6274 2.04139 16.253 2.41576L8.37242 9.60639L4.93805 7.00201C4.6193 6.75764 4.16992 6.77764 3.87367 7.04764L2.77367 8.05014C2.4093 8.37889 2.4093 8.95201 2.77055 9.28076L5.7493 11.9989L2.77055 14.717C2.4093 15.0458 2.4093 15.6189 2.77367 15.9476L3.87367 16.9501C4.17305 17.2201 4.6193 17.2401 4.93805 16.9958L8.37242 14.3883L16.2568 21.582C16.628 21.9564 17.1974 22.0539 17.6762 21.8226L21.7943 19.8401C22.2274 19.632 22.5005 19.1958 22.5005 18.7139V5.27951C22.5005 4.80076 22.2237 4.36139 21.7912 4.15326L21.7899 4.15451ZM17.5024 16.5408L11.5193 11.9995L17.5024 7.45826V16.5408Z"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="clip0_2614_159777">
<rect width="20" height="20" fill="white" transform="translate(2.5 2)" />
</clipPath>
</defs>
</svg>
</span>
<span>VS Code</span>
</div>
<a href="https://opencode.ai/docs/ide/" data-component="action-button">
Install
</a>
</div>
<div data-component="download-row">
<div data-component="download-info">
<span data-slot="icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2614_159762)">
<path
d="M20.1613 6.73388L12.4027 2.11135C12.1535 1.96288 11.8461 1.96288 11.597 2.11135L3.83874 6.73388C3.6293 6.85867 3.5 7.08946 3.5 7.33942V16.6608C3.5 16.9107 3.6293 17.1415 3.83874 17.2663L11.5973 21.8888C11.8465 22.0373 12.1539 22.0373 12.403 21.8888L20.1616 17.2663C20.3711 17.1415 20.5004 16.9107 20.5004 16.6608V7.33942C20.5004 7.08946 20.3711 6.85867 20.1616 6.73388H20.1613ZM19.6739 7.71304L12.1841 21.1002C12.1335 21.1905 11.9998 21.1536 11.9998 21.0491V12.2833C11.9998 12.1082 11.9091 11.9462 11.762 11.8582L4.40586 7.47548C4.31844 7.42324 4.35413 7.28529 4.45539 7.28529H19.435C19.6477 7.28529 19.7806 7.52322 19.6743 7.71341H19.6739V7.71304Z"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="clip0_2614_159762">
<rect width="17" height="20" fill="white" transform="translate(3.5 2)" />
</clipPath>
</defs>
</svg>
</span>
<span>Cursor</span>
</div>
<a href="https://opencode.ai/docs/ide/" data-component="action-button">
Install
</a>
</div>
<div data-component="download-row">
<div data-component="download-info">
<span data-slot="icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M4.375 3.25C4.02982 3.25 3.75 3.52982 3.75 3.875V17.625H2.5V3.875C2.5 2.83947 3.33947 2 4.375 2H21.1206C21.9558 2 22.374 3.00982 21.7835 3.60042L11.4698 13.9141H14.375V12.625H15.625V14.2266C15.625 14.7443 15.2053 15.1641 14.6875 15.1641H10.2198L8.07139 17.3125H17.8125V9.5H19.0625V17.3125C19.0625 18.0029 18.5029 18.5625 17.8125 18.5625H6.82139L4.63389 20.75H20.625C20.9701 20.75 21.25 20.4701 21.25 20.125V6.375H22.5V20.125C22.5 21.1606 21.6606 22 20.625 22H3.87944C3.04422 22 2.62594 20.9901 3.21653 20.3996L13.4911 10.125H10.625V11.375H9.375V9.8125C9.375 9.29474 9.79474 8.875 10.3125 8.875H14.7411L16.9286 6.6875H7.1875V14.5H5.9375V6.6875C5.9375 5.99714 6.49714 5.4375 7.1875 5.4375H18.1786L20.3661 3.25H4.375Z"
fill="currentColor"
/>
</svg>
</span>
<span>Zed</span>
</div>
<a href="https://opencode.ai/docs/ide/" data-component="action-button">
Install
</a>
</div>
<div data-component="download-row">
<div data-component="download-info">
<span data-slot="icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M21.8156 6.00325H21.625C20.6219 6.00162 19.8079 6.8448 19.8079 7.88581V12.0961C19.8079 12.9368 19.1384 13.6179 18.3415 13.6179C17.8681 13.6179 17.3955 13.3706 17.115 12.9555L12.9722 6.814C12.6285 6.30403 12.0691 6 11.4637 6C10.5192 6 9.66922 6.83345 9.66922 7.86232V12.0969C9.66922 12.9376 9.00519 13.6187 8.20289 13.6187C7.72791 13.6187 7.25603 13.3714 6.97557 12.9563L2.33983 6.08351C2.23514 5.92783 2 6.00487 2 6.1946V9.86649C2 10.0522 2.05469 10.2322 2.15702 10.3846L6.71933 17.1471C6.98886 17.5468 7.38651 17.8435 7.84507 17.9514C8.9927 18.2221 10.0489 17.3052 10.0489 16.1369V11.9047C10.0489 11.064 10.7051 10.3829 11.5152 10.3829H11.5176C12.0059 10.3829 12.4636 10.6302 12.7441 11.0453L16.8877 17.186C17.2322 17.6968 17.7627 18 18.3954 18C19.361 18 20.1883 17.1657 20.1883 16.1377V11.9039C20.1883 11.0632 20.8446 10.3821 21.6547 10.3821H21.8164C21.9179 10.3821 22 10.297 22 10.1916V6.19377C22 6.08839 21.9179 6.00325 21.8164 6.00325H21.8156Z"
fill="currentColor"
/>
</svg>
</span>
<span>Windsurf</span>
</div>
<a href="https://opencode.ai/docs/ide/" data-component="action-button">
Install
</a>
</div>
<div data-component="download-row">
<div data-component="download-info">
<span data-slot="icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M11.6179 1.49887C10.99 1.90169 10.8089 2.73615 11.2135 3.36183C13.4375 6.80593 13.9624 9.40369 13.7347 11.6802C12.8142 16.0398 10.8133 16.9242 9.06476 16.9242C7.35756 16.9242 7.81472 14.1145 9.09798 13.2922C9.86402 12.8139 10.8452 12.503 11.5983 12.503C12.3445 12.503 12.9495 11.9 12.9495 11.156C12.9495 10.4117 12.3445 9.80871 11.5983 9.80871C10.7187 9.80871 9.85588 9.99351 9.05046 10.3081C9.21502 9.53173 9.27574 8.69265 9.063 7.80077C8.74004 6.44645 7.81032 5.15285 6.19596 3.89885C5.91326 3.67885 5.55466 3.58007 5.19892 3.62407C4.84318 3.66807 4.51956 3.85111 4.29934 4.13315C3.8413 4.72055 3.94734 5.56711 4.5365 6.02405C5.85166 7.04551 6.28594 7.80165 6.43444 8.42403C6.58294 9.04641 6.46348 9.71411 6.16516 10.6315C5.7839 11.8679 5.34126 12.9716 5.14722 14.0301C5.05174 14.551 5.0436 15.118 5.01896 15.5709C4.07186 14.6478 3.70116 13.429 3.70116 11.6481C3.70094 10.9041 3.09594 10.3008 2.34992 10.3011C1.60434 10.3017 1.00022 10.9045 1 11.6481C1 14.0804 1.71126 16.3948 3.61756 17.9388C5.34324 19.5829 9.73158 18.9752 9.73158 21.6146C9.73158 22.3595 10.8219 22.722 11.5679 22.722C12.3331 22.722 13.296 22.2105 13.296 21.6146C13.296 18.6199 16.4519 16.7999 21.6472 16.8078C22.3935 16.8089 22.9989 16.2063 23 15.4623C23.0013 14.718 22.3976 14.1137 21.6514 14.1123C21.2961 14.1119 20.9498 14.124 20.6084 14.1442C21.1892 12.7783 21.4468 11.2743 21.3936 9.64987C21.3689 8.90605 20.7446 8.32305 19.999 8.34725C19.2525 8.37145 18.6678 8.99471 18.6922 9.73897C18.7626 11.8659 18.6829 13.7652 17.0983 14.7664C16.6477 15.0509 16.1239 15.2977 15.6271 15.2977C16.0128 14.2487 16.3041 13.1415 16.4233 11.948C16.4994 11.1863 16.5076 10.2815 16.4207 9.57859C16.2858 8.48959 16.123 7.25451 16.5364 6.32413C16.9078 5.52289 17.7398 5.18739 18.9615 5.18739C19.707 5.18673 20.3112 4.58371 20.3114 3.84033C20.3118 3.09607 19.7075 2.49239 18.9615 2.49173C17.146 2.49173 15.7699 3.44719 14.9898 4.60153C14.5819 3.73033 14.0852 2.83251 13.485 1.90323C13.2912 1.60293 12.9858 1.39195 12.6358 1.31605C12.4624 1.27843 12.2834 1.27513 12.1087 1.30637C11.934 1.33783 11.7672 1.40317 11.6179 1.49887Z"
fill="currentColor"
/>
</svg>
</span>
<span>VSCodium</span>
</div>
<a href="https://opencode.ai/docs/ide/" data-component="action-button">
Install
</a>
</div>
</div>
</section>
<section data-component="download-section">
<div data-component="section-label">
<span>[4]</span> OpenCode Integrations
</div>
<div data-component="section-content">
<div data-component="download-row">
<div data-component="download-info">
<span data-slot="icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M12 1.94922C17.525 1.94922 22 6.42422 22 11.9492C21.9995 14.0445 21.3419 16.0868 20.1198 17.7887C18.8977 19.4907 17.1727 20.7665 15.1875 21.4367C14.6875 21.5367 14.5 21.2242 14.5 20.9617C14.5 20.6242 14.5125 19.5492 14.5125 18.2117C14.5125 17.2742 14.2 16.6742 13.8375 16.3617C16.0625 16.1117 18.4 15.2617 18.4 11.4242C18.4 10.3242 18.0125 9.43672 17.375 8.73672C17.475 8.48672 17.825 7.46172 17.275 6.08672C17.275 6.08672 16.4375 5.81172 14.525 7.11172C13.725 6.88672 12.875 6.77422 12.025 6.77422C11.175 6.77422 10.325 6.88672 9.525 7.11172C7.6125 5.82422 6.775 6.08672 6.775 6.08672C6.225 7.46172 6.575 8.48672 6.675 8.73672C6.0375 9.43672 5.65 10.3367 5.65 11.4242C5.65 15.2492 7.975 16.1117 10.2 16.3617C9.9125 16.6117 9.65 17.0492 9.5625 17.6992C8.9875 17.9617 7.55 18.3867 6.65 16.8742C6.4625 16.5742 5.9 15.8367 5.1125 15.8492C4.275 15.8617 4.775 16.3242 5.125 16.5117C5.55 16.7492 6.0375 17.6367 6.15 17.9242C6.35 18.4867 7 19.5617 9.5125 19.0992C9.5125 19.9367 9.525 20.7242 9.525 20.9617C9.525 21.2242 9.3375 21.5242 8.8375 21.4367C6.8458 20.7738 5.11342 19.5005 3.88611 17.7975C2.65881 16.0945 1.9989 14.0484 2 11.9492C2 6.42422 6.475 1.94922 12 1.94922Z"
fill="currentColor"
/>
</svg>
</span>
<span>GitHub</span>
</div>
<a href="https://opencode.ai/docs/github/" data-component="action-button">
Install
</a>
</div>
<div data-component="download-row">
<div data-component="download-info">
<span data-slot="icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M20.7011 10.1255L20.6758 10.0583L18.2257 3.41877C18.1759 3.28864 18.0876 3.17824 17.9736 3.10343C17.8595 3.02989 17.7264 2.99447 17.5924 3.00196C17.4583 3.00944 17.3296 3.05947 17.2238 3.14528C17.1191 3.23356 17.0432 3.35318 17.0063 3.48787L15.352 8.74347H8.65334L6.99905 3.48787C6.96317 3.35245 6.88708 3.23223 6.7816 3.14431C6.67576 3.05849 6.54711 3.00847 6.41303 3.00098C6.27894 2.9935 6.14587 3.02892 6.03178 3.10246C5.91802 3.17757 5.82983 3.28787 5.77965 3.4178L3.32493 10.0545L3.30056 10.1216C2.94787 11.0785 2.90433 12.1286 3.17652 13.1134C3.44871 14.0983 4.02187 14.9645 4.80957 15.5816L4.81801 15.5884L4.8405 15.605L8.57273 18.5072L10.4192 19.9584L11.5439 20.8401C11.6755 20.9438 11.8361 21 12.0013 21C12.1665 21 12.3271 20.9438 12.4587 20.8401L13.5834 19.9584L15.4298 18.5072L19.1846 15.5874L19.1939 15.5797C19.9799 14.9625 20.5517 14.0971 20.8235 13.1136C21.0952 12.1301 21.0523 11.0815 20.7011 10.1255Z"
fill="currentColor"
/>
</svg>
</span>
<span>GitLab</span>
</div>
<a href="https://opencode.ai/docs/gitlab/" data-component="action-button">
Install
</a>
</div>
</div>
</section>
</div>
<section data-component="faq">
<div data-slot="section-title">
<h3>FAQ</h3>
</div>
<ul>
<li>
<Faq question="What is OpenCode?">
OpenCode is an open source agent that helps you write and run code with any AI model. It's available as
a terminal-based interface, desktop app, or IDE extension.
</Faq>
</li>
<li>
<Faq question="How do I use OpenCode?">
The easiest way to get started is to read the <a href="/docs">intro</a>.
</Faq>
</li>
<li>
<Faq question="Do I need extra AI subscriptions to use OpenCode?">
Not necessarily, but probably. You'll need an AI subscription if you want to connect OpenCode to a paid
provider, although you can work with{" "}
<a href="/docs/providers/#lm-studio" target="_blank">
local models
</a>{" "}
for free. While we encourage users to use <A href="/zen">Zen</A>, OpenCode works with all popular
providers such as OpenAI, Anthropic, xAI etc.
</Faq>
</li>
<li>
<Faq question="Can I only use OpenCode in the terminal?">
Not anymore! OpenCode is now available as an app for your desktop.
</Faq>
</li>
<li>
<Faq question="How much does OpenCode cost?">
OpenCode is 100% free to use. Any additional costs will come from your subscription to a model provider.
While OpenCode works with any model provider, we recommend using <A href="/zen">Zen</A>.
</Faq>
</li>
<li>
<Faq question="What about data and privacy?">
Your data and information is only stored when you create sharable links in OpenCode. Learn more about{" "}
<a href="/docs/share/#privacy">share pages</a>.
</Faq>
</li>
<li>
<Faq question="Is OpenCode open source?">
Yes, OpenCode is fully open source. The source code is public on{" "}
<a href={config.github.repoUrl} target="_blank">
GitHub
</a>{" "}
under the{" "}
<a href={`${config.github.repoUrl}?tab=MIT-1-ov-file#readme`} target="_blank">
MIT License
</a>
, meaning anyone can use, modify, or contribute to its development. Anyone from the community can file
issues, submit pull requests, and extend functionality.
</Faq>
</li>
</ul>
</section>
<Footer />
</div>
<Legal />
</main>
)
}
@@ -84,7 +84,16 @@
ul {
display: flex;
justify-content: space-between;
align-items: center;
gap: 48px;
@media (max-width: 55rem) {
gap: 32px;
}
@media (max-width: 48rem) {
gap: 24px;
}
li {
display: inline-block;
a {
@@ -98,6 +107,22 @@
text-underline-offset: 2px;
text-decoration-thickness: 1px;
}
[data-slot="cta-button"] {
background: var(--color-background-strong);
color: var(--color-text-inverted);
padding: 8px 16px;
border-radius: 4px;
font-weight: 500;
text-decoration: none;
@media (max-width: 55rem) {
display: none;
}
}
[data-slot="cta-button"]:hover {
background: var(--color-background-strong-hover);
text-decoration: none;
}
}
}
@@ -289,7 +314,7 @@
[data-component="enterprise-column-1"] {
h1 {
font-size: 1.5rem;
font-weight: 500;
font-weight: 700;
color: var(--color-text-strong);
margin-bottom: 1rem;
}
@@ -441,7 +466,7 @@
h3 {
font-size: 16px;
font-weight: 500;
font-weight: 700;
color: var(--color-text-strong);
margin-bottom: 12px;
}
+117 -9
View File
@@ -16,6 +16,8 @@
--color-background-strong-hover: hsl(0, 5%, 18%);
--color-background-interactive: hsl(62, 84%, 88%);
--color-background-interactive-weaker: hsl(64, 74%, 95%);
--color-surface-raised-base: hsla(0, 100%, 3%, 0.01);
--color-surface-raised-base-active: hsla(0, 100%, 17%, 0.06);
--color-text: hsl(0, 1%, 39%);
--color-text-weak: hsl(0, 1%, 60%);
@@ -24,7 +26,7 @@
--color-text-inverted: hsl(0, 20%, 99%);
--color-border: hsl(30, 2%, 81%);
--color-border-weak: hsl(0, 1%, 85%);
--color-border-weak: hsla(0, 100%, 3%, 0.12);
--color-icon: hsl(0, 1%, 55%);
}
@@ -62,6 +64,14 @@ body {
}
}
[data-slot="br"] {
display: block;
@media (max-width: 60rem) {
display: none;
}
}
[data-page="opencode"] {
background: var(--color-background);
--padding: 5rem;
@@ -215,7 +225,16 @@ body {
ul {
display: flex;
justify-content: space-between;
align-items: center;
gap: 48px;
@media (max-width: 55rem) {
gap: 32px;
}
@media (max-width: 48rem) {
gap: 24px;
}
li {
display: inline-block;
a {
@@ -229,6 +248,25 @@ body {
text-underline-offset: var(--space-1);
text-decoration-thickness: 1px;
}
[data-slot="cta-button"] {
background: var(--color-background-strong);
color: var(--color-text-inverted);
padding: 8px 16px 8px 10px;
border-radius: 4px;
font-weight: 500;
text-decoration: none;
display: flex;
align-items: center;
gap: 8px;
@media (max-width: 55rem) {
display: none;
}
}
[data-slot="cta-button"]:hover {
background: var(--color-background-strong-hover);
text-decoration: none;
}
}
}
@@ -322,7 +360,7 @@ body {
display: flex;
flex-direction: column;
max-width: 100%;
padding: calc(var(--vertical-padding) * 2) var(--padding);
padding: calc(var(--vertical-padding) * 1.5) var(--padding);
@media (max-width: 30rem) {
padding: var(--vertical-padding) var(--padding);
@@ -426,7 +464,7 @@ body {
cursor: pointer;
align-items: center;
color: var(--color-text);
gap: var(--space-1);
gap: 16px;
color: var(--color-text);
padding: 8px 16px 8px 8px;
border-radius: 4px;
@@ -465,6 +503,77 @@ body {
}
}
[data-component="desktop-app-banner"] {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 32px;
[data-slot="badge"] {
background: var(--color-background-strong);
color: var(--color-text-inverted);
font-weight: 500;
padding: 4px 8px;
line-height: 1;
flex-shrink: 0;
}
[data-slot="content"] {
display: flex;
align-items: center;
gap: 4px;
}
[data-slot="text"] {
color: var(--color-text-strong);
line-height: 1.4;
@media (max-width: 30.625rem) {
display: none;
}
}
[data-slot="platforms"] {
@media (max-width: 49.125rem) {
display: none;
}
}
[data-slot="link"] {
color: var(--color-text-weak);
white-space: nowrap;
text-decoration: none;
@media (max-width: 30.625rem) {
display: none;
}
}
[data-slot="link"]:hover {
color: var(--color-text);
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
}
[data-slot="link-mobile"] {
display: none;
color: var(--color-text-strong);
white-space: nowrap;
text-decoration: none;
@media (max-width: 30.625rem) {
display: inline;
}
}
[data-slot="link-mobile"]:hover {
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
}
}
[data-slot="hero-copy"] {
[data-slot="releases"] {
background: none;
@@ -492,7 +601,7 @@ body {
h1 {
font-size: 38px;
color: var(--color-text-strong);
font-weight: 500;
font-weight: 700;
margin-bottom: 8px;
@media (max-width: 60rem) {
@@ -502,7 +611,7 @@ body {
p {
color: var(--color-text);
margin-bottom: 40px;
margin-bottom: 32px;
max-width: 82%;
@media (max-width: 50rem) {
@@ -518,7 +627,6 @@ body {
border-radius: 4px;
font-weight: 500;
cursor: pointer;
margin-bottom: 80px;
display: flex;
width: fit-content;
gap: 12px;
@@ -596,7 +704,7 @@ body {
h3 {
font-size: 16px;
font-weight: 500;
font-weight: 700;
color: var(--color-text-strong);
margin-bottom: 12px;
}
@@ -701,7 +809,7 @@ body {
[data-slot="privacy-title"] {
h3 {
font-size: 16px;
font-weight: 500;
font-weight: 700;
color: var(--color-text-strong);
margin-bottom: 12px;
}
@@ -727,7 +835,7 @@ body {
[data-slot="zen-cta-copy"] {
strong {
color: var(--color-text-strong);
font-weight: 500;
font-weight: 700;
margin-bottom: 16px;
display: block;
}
+44 -20
View File
@@ -53,16 +53,17 @@ export default function Home() {
<div data-component="content">
<section data-component="hero">
<div data-slot="hero-copy">
<a data-slot="releases" href={release()?.url ?? `${config.github.repoUrl}/releases`} target="_blank">
Whats new in {release()?.name ?? "the latest release"}
</a>
<h1>The open source coding agent</h1>
{/*<a data-slot="releases"*/}
{/* href={release()?.url ?? `${config.github.repoUrl}/releases`}*/}
{/* target="_blank">*/}
{/* Whats new in {release()?.name ?? "the latest release"}*/}
{/*</a>*/}
<h1>The open source AI coding agent</h1>
<p>
OpenCode includes free models or connect from any provider to <br />
use other models, including Claude, GPT, Gemini and more.
Free models included or connect any model from any provider, <span data-slot="br"></span>including
Claude, GPT, Gemini and more.
</p>
</div>
<p data-slot="installation-instructions">Install and use. No account, no email, and no credit card.</p>
<div data-slot="installation">
<Tabs
as="section"
@@ -141,11 +142,6 @@ export default function Home() {
</div>
</Tabs>
</div>
<p data-slot="installation-options">
Available in terminal, web, and desktop (coming soon).
<br />
Extensions for VS Code, Cursor, Windsurf, and more.
</p>
</section>
<section data-component="video">
@@ -656,13 +652,21 @@ export default function Home() {
</li>
<li>
<Faq question="Do I need extra AI subscriptions to use OpenCode?">
Not necessarily, but probably. Youll need an AI subscription if you want to connect OpenCode to a
paid provider, although you can work with{" "}
Not necessarily, OpenCode comes with a set of free models that you can use without creating an
account. Aside from these, you can use any of the popular coding models by creating a{" "}
<A href="/zen">Zen</A> account. While we encourage users to use Zen, OpenCode also works with all
popular providers such as OpenAI, Anthropic, xAI etc. You can even connect your{" "}
<a href="/docs/providers/#lm-studio" target="_blank">
local models
</a>{" "}
for free. While we encourage users to use <A href="/zen">Zen</A>, OpenCode works with all popular
providers such as OpenAI, Anthropic, xAI etc.
</a>
.
</Faq>
</li>
<li>
<Faq question="Can I use my existing AI subscriptions with OpenCode?">
Yes, OpenCode supports subscription plans from all major providers. You can use your Claude Pro/Max,
ChatGPT Plus/Pro, or GitHub Copilot subscriptions. <a href="/docs/providers/#directory">Learn more</a>
.
</Faq>
</li>
<li>
@@ -672,13 +676,14 @@ export default function Home() {
</li>
<li>
<Faq question="How much does OpenCode cost?">
OpenCode is 100% free to use. Any additional costs will come from your subscription to a model
provider. While OpenCode works with any model provider, we recommend using <A href="/zen">Zen</A>.
OpenCode is 100% free to use. It also comes with a set of free models. There might be additional costs
if you connect any other provider.
</Faq>
</li>
<li>
<Faq question="What about data and privacy?">
Your data and information is only stored when you create sharable links in OpenCode. Learn more about{" "}
Your data and information is only stored when you use our free models or create sharable links. Learn
more about <a href="/docs/zen/#privacy">our models</a> and{" "}
<a href="/docs/share/#privacy">share pages</a>.
</Faq>
</li>
@@ -738,6 +743,17 @@ export default function Home() {
/>
</svg>
</div>
<div>
<svg
width="24"
height="24"
viewBox="0 0 50 50"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M49.04,24.001l-1.082-0.043h-0.001C36.134,23.492,26.508,13.866,26.042,2.043L25.999,0.96C25.978,0.424,25.537,0,25,0 s-0.978,0.424-0.999,0.96l-0.043,1.083C23.492,13.866,13.866,23.492,2.042,23.958L0.96,24.001C0.424,24.022,0,24.463,0,25 c0,0.537,0.424,0.978,0.961,0.999l1.082,0.042c11.823,0.467,21.449,10.093,21.915,21.916l0.043,1.083C24.022,49.576,24.463,50,25,50 s0.978-0.424,0.999-0.96l0.043-1.083c0.466-11.823,10.092-21.449,21.915-21.916l1.082-0.042C49.576,25.978,50,25.537,50,25 C50,24.463,49.576,24.022,49.04,24.001z"></path>
</svg>
</div>
<div>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
@@ -768,6 +784,14 @@ export default function Home() {
/>
</svg>
</div>
<div>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M12.0962 3L10.0998 5.6577H1.59858L3.59417 3H12.0972H12.0962ZM22.3162 18.3432L20.3215 21H11.8497L13.8425 18.3432H22.3162ZM23 3L9.492 21H1L14.508 3H23Z"
fill="black"
/>
</svg>
</div>
</div>
<A href="/zen">
<span>Learn about Zen </span>
@@ -43,9 +43,12 @@ const getModelsInfo = query(async (workspaceID: string) => {
const pA = getPriority(idA)
const pB = getPriority(idB)
if (pA !== pB) return pA - pB
return modelA.name.localeCompare(modelB.name)
const modelAName = Array.isArray(modelA) ? modelA[0].name : modelA.name
const modelBName = Array.isArray(modelB) ? modelB[0].name : modelB.name
return modelAName.localeCompare(modelBName)
})
.map(([id, model]) => ({ id, name: model.name })),
.map(([id, model]) => ({ id, name: Array.isArray(model) ? model[0].name : model.name })),
disabled: await Model.listDisabled(),
}
}, workspaceID)
+28 -3
View File
@@ -147,7 +147,16 @@ body {
ul {
display: flex;
justify-content: space-between;
align-items: center;
gap: 48px;
@media (max-width: 55rem) {
gap: 32px;
}
@media (max-width: 48rem) {
gap: 24px;
}
li {
display: inline-block;
a {
@@ -161,6 +170,22 @@ body {
text-underline-offset: var(--space-1);
text-decoration-thickness: 1px;
}
[data-slot="cta-button"] {
background: var(--color-background-strong);
color: var(--color-text-inverted);
padding: 8px 16px;
border-radius: 4px;
font-weight: 500;
text-decoration: none;
@media (max-width: 55rem) {
display: none;
}
}
[data-slot="cta-button"]:hover {
background: var(--color-background-strong-hover);
text-decoration: none;
}
}
}
@@ -280,7 +305,7 @@ body {
h1 {
font-size: 28px;
color: var(--color-text-strong);
font-weight: 500;
font-weight: 700;
margin-bottom: 16px;
display: block;
@@ -369,7 +394,7 @@ body {
h3 {
font-size: 16px;
font-weight: 500;
font-weight: 700;
color: var(--color-text-strong);
margin-bottom: 12px;
}
@@ -442,7 +467,7 @@ body {
[data-slot="privacy-title"] {
h3 {
font-size: 16px;
font-weight: 500;
font-weight: 700;
color: var(--color-text);
margin-bottom: 12px;
}
@@ -38,7 +38,7 @@ export default function Home() {
<Meta name="opencode:auth" content={loggedin() ? "true" : "false"} />
<div data-component="container">
<Header zen />
<Header zen hideGetStarted />
<div data-component="content">
<section data-component="hero">
@@ -57,15 +57,17 @@ export async function handler(
const sessionId = input.request.headers.get("x-opencode-session") ?? ""
const requestId = input.request.headers.get("x-opencode-request") ?? ""
const projectId = input.request.headers.get("x-opencode-project") ?? ""
const ocClient = input.request.headers.get("x-opencode-client") ?? ""
logger.metric({
is_tream: isStream,
session: sessionId,
request: requestId,
client: ocClient,
})
const zenData = ZenData.list()
const modelInfo = validateModel(zenData, model)
const dataDumper = createDataDumper(sessionId, requestId, projectId)
const trialLimiter = createTrialLimiter(modelInfo.trial?.limit, ip)
const trialLimiter = createTrialLimiter(modelInfo.trial, ip, ocClient)
const isTrial = await trialLimiter?.isTrial()
const rateLimiter = createRateLimiter(modelInfo.id, modelInfo.rateLimit, ip)
await rateLimiter?.check()
@@ -286,11 +288,14 @@ export async function handler(
}
function validateModel(zenData: ZenData, reqModel: string) {
if (!(reqModel in zenData.models)) {
throw new ModelError(`Model ${reqModel} not supported`)
}
if (!(reqModel in zenData.models)) throw new ModelError(`Model ${reqModel} not supported`)
const modelId = reqModel as keyof typeof zenData.models
const modelData = zenData.models[modelId]
const modelData = Array.isArray(zenData.models[modelId])
? zenData.models[modelId].find((model) => opts.format === model.formatFilter)
: zenData.models[modelId]
if (!modelData) throw new ModelError(`Model ${reqModel} not supported for format ${opts.format}`)
logger.metric({ model: modelId })
@@ -1,12 +1,18 @@
import { Database, eq, sql } from "@opencode-ai/console-core/drizzle/index.js"
import { IpTable } from "@opencode-ai/console-core/schema/ip.sql.js"
import { UsageInfo } from "./provider/provider"
import { ZenData } from "@opencode-ai/console-core/model.js"
export function createTrialLimiter(limit: number | undefined, ip: string) {
if (!limit) return
export function createTrialLimiter(trial: ZenData.Trial | undefined, ip: string, client: string) {
if (!trial) return
if (!ip) return
let trial: boolean
const limit =
trial.limits.find((limit) => limit.client === client)?.limit ??
trial.limits.find((limit) => limit.client === undefined)?.limit
if (!limit) return
let _isTrial: boolean
return {
isTrial: async () => {
@@ -20,11 +26,11 @@ export function createTrialLimiter(limit: number | undefined, ip: string) {
.then((rows) => rows[0]),
)
trial = (data?.usage ?? 0) < limit
return trial
_isTrial = (data?.usage ?? 0) < limit
return _isTrial
},
track: async (usageInfo: UsageInfo) => {
if (!trial) return
if (!_isTrial) return
const usage =
usageInfo.inputTokens +
usageInfo.outputTokens +
@@ -15,6 +15,7 @@ body {
--font-size-9xl: 8rem;
--font-mono:
"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
"Berkeley Mono", "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
--font-sans: var(--font-mono);
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.0.145",
"version": "1.0.153",
"private": true,
"type": "module",
"dependencies": {
@@ -16,16 +16,19 @@ const value1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[
const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
const value5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
if (!value1) throw new Error("ZEN_MODELS1 not found")
if (!value2) throw new Error("ZEN_MODELS2 not found")
if (!value3) throw new Error("ZEN_MODELS3 not found")
if (!value4) throw new Error("ZEN_MODELS4 not found")
if (!value5) throw new Error("ZEN_MODELS5 not found")
// validate value
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4))
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5))
// update the secret
await $`bun sst secret set ZEN_MODELS1 ${value1} --stage ${stage}`
await $`bun sst secret set ZEN_MODELS2 ${value2} --stage ${stage}`
await $`bun sst secret set ZEN_MODELS3 ${value3} --stage ${stage}`
await $`bun sst secret set ZEN_MODELS4 ${value4} --stage ${stage}`
await $`bun sst secret set ZEN_MODELS5 ${value5} --stage ${stage}`
+4 -1
View File
@@ -16,16 +16,19 @@ const value1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=")[
const value2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
const value5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
if (!value1) throw new Error("ZEN_MODELS1 not found")
if (!value2) throw new Error("ZEN_MODELS2 not found")
if (!value3) throw new Error("ZEN_MODELS3 not found")
if (!value4) throw new Error("ZEN_MODELS4 not found")
if (!value5) throw new Error("ZEN_MODELS5 not found")
// validate value
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4))
ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5))
// update the secret
await $`bun sst secret set ZEN_MODELS1 ${value1}`
await $`bun sst secret set ZEN_MODELS2 ${value2}`
await $`bun sst secret set ZEN_MODELS3 ${value3}`
await $`bun sst secret set ZEN_MODELS4 ${value4}`
await $`bun sst secret set ZEN_MODELS5 ${value5}`
@@ -14,15 +14,17 @@ const oldValue1 = lines.find((line) => line.startsWith("ZEN_MODELS1"))?.split("=
const oldValue2 = lines.find((line) => line.startsWith("ZEN_MODELS2"))?.split("=")[1]
const oldValue3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[1]
const oldValue4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
const oldValue5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
if (!oldValue1) throw new Error("ZEN_MODELS1 not found")
if (!oldValue2) throw new Error("ZEN_MODELS2 not found")
if (!oldValue3) throw new Error("ZEN_MODELS3 not found")
if (!oldValue4) throw new Error("ZEN_MODELS4 not found")
if (!oldValue5) throw new Error("ZEN_MODELS5 not found")
// store the prettified json to a temp file
const filename = `models-${Date.now()}.json`
const tempFile = Bun.file(path.join(os.tmpdir(), filename))
await tempFile.write(JSON.stringify(JSON.parse(oldValue1 + oldValue2 + oldValue3 + oldValue4), null, 2))
await tempFile.write(JSON.stringify(JSON.parse(oldValue1 + oldValue2 + oldValue3 + oldValue4 + oldValue5), null, 2))
console.log("tempFile", tempFile.name)
// open temp file in vim and read the file on close
@@ -31,12 +33,15 @@ const newValue = JSON.stringify(JSON.parse(await tempFile.text()))
ZenData.validate(JSON.parse(newValue))
// update the secret
const chunk = Math.ceil(newValue.length / 4)
const chunk = Math.ceil(newValue.length / 5)
const newValue1 = newValue.slice(0, chunk)
const newValue2 = newValue.slice(chunk, chunk * 2)
const newValue3 = newValue.slice(chunk * 2, chunk * 3)
const newValue4 = newValue.slice(chunk * 3)
const newValue4 = newValue.slice(chunk * 3, chunk * 4)
const newValue5 = newValue.slice(chunk * 4)
await $`bun sst secret set ZEN_MODELS1 ${newValue1}`
await $`bun sst secret set ZEN_MODELS2 ${newValue2}`
await $`bun sst secret set ZEN_MODELS3 ${newValue3}`
await $`bun sst secret set ZEN_MODELS4 ${newValue4}`
await $`bun sst secret set ZEN_MODELS5 ${newValue5}`
+17 -8
View File
@@ -9,7 +9,17 @@ import { Resource } from "@opencode-ai/console-resource"
export namespace ZenData {
const FormatSchema = z.enum(["anthropic", "google", "openai", "oa-compat"])
const TrialSchema = z.object({
provider: z.string(),
limits: z.array(
z.object({
limit: z.number(),
client: z.enum(["cli", "desktop"]).optional(),
}),
),
})
export type Format = z.infer<typeof FormatSchema>
export type Trial = z.infer<typeof TrialSchema>
const ModelCostSchema = z.object({
input: z.number(),
@@ -26,12 +36,7 @@ export namespace ZenData {
allowAnonymous: z.boolean().optional(),
byokProvider: z.enum(["openai", "anthropic", "google"]).optional(),
stickyProvider: z.boolean().optional(),
trial: z
.object({
limit: z.number(),
provider: z.string(),
})
.optional(),
trial: TrialSchema.optional(),
rateLimit: z.number().optional(),
fallbackProvider: z.string().optional(),
providers: z.array(
@@ -53,7 +58,7 @@ export namespace ZenData {
})
const ModelsSchema = z.object({
models: z.record(z.string(), ModelSchema),
models: z.record(z.string(), z.union([ModelSchema, z.array(ModelSchema.extend({ formatFilter: FormatSchema }))])),
providers: z.record(z.string(), ProviderSchema),
})
@@ -63,7 +68,11 @@ export namespace ZenData {
export const list = fn(z.void(), () => {
const json = JSON.parse(
Resource.ZEN_MODELS1.value + Resource.ZEN_MODELS2.value + Resource.ZEN_MODELS3.value + Resource.ZEN_MODELS4.value,
Resource.ZEN_MODELS1.value +
Resource.ZEN_MODELS2.value +
Resource.ZEN_MODELS3.value +
Resource.ZEN_MODELS4.value +
Resource.ZEN_MODELS5.value,
)
return ModelsSchema.parse(json)
})
+8 -4
View File
@@ -50,10 +50,6 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"Enterprise": {
"type": "sst.cloudflare.SolidStart"
"url": string
}
"GITHUB_APP_ID": {
"type": "sst.sst.Secret"
"value": string
@@ -94,6 +90,10 @@ declare module "sst" {
"type": "sst.sst.Linkable"
"value": string
}
"Teams": {
"type": "sst.cloudflare.SolidStart"
"url": string
}
"Web": {
"type": "sst.cloudflare.Astro"
"url": string
@@ -114,6 +114,10 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS5": {
"type": "sst.sst.Secret"
"value": string
}
}
}
// cloudflare
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.0.145",
"version": "1.0.153",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+8 -4
View File
@@ -50,10 +50,6 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"Enterprise": {
"type": "sst.cloudflare.SolidStart"
"url": string
}
"GITHUB_APP_ID": {
"type": "sst.sst.Secret"
"value": string
@@ -94,6 +90,10 @@ declare module "sst" {
"type": "sst.sst.Linkable"
"value": string
}
"Teams": {
"type": "sst.cloudflare.SolidStart"
"url": string
}
"Web": {
"type": "sst.cloudflare.Astro"
"url": string
@@ -114,6 +114,10 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS5": {
"type": "sst.sst.Secret"
"value": string
}
}
}
// cloudflare
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.0.145",
"version": "1.0.153",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+8 -4
View File
@@ -50,10 +50,6 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"Enterprise": {
"type": "sst.cloudflare.SolidStart"
"url": string
}
"GITHUB_APP_ID": {
"type": "sst.sst.Secret"
"value": string
@@ -94,6 +90,10 @@ declare module "sst" {
"type": "sst.sst.Linkable"
"value": string
}
"Teams": {
"type": "sst.cloudflare.SolidStart"
"url": string
}
"Web": {
"type": "sst.cloudflare.Astro"
"url": string
@@ -114,6 +114,10 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS5": {
"type": "sst.sst.Secret"
"value": string
}
}
}
// cloudflare
+2 -2
View File
@@ -14,7 +14,7 @@
<meta property="og:image" content="/social-share.png" />
<meta property="twitter:image" content="/social-share.png" />
</head>
<body class="antialiased overscroll-none select-none text-12-regular">
<body class="antialiased overscroll-none select-none text-12-regular overflow-hidden">
<script>
;(function () {
const savedTheme = localStorage.getItem("theme") || "oc-1"
@@ -22,7 +22,7 @@
})()
</script>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<div id="root" class="flex flex-col h-screen"></div>
<script src="/src/entry.tsx" type="module"></script>
</body>
</html>
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/desktop",
"version": "1.0.145",
"version": "1.0.153",
"description": "",
"type": "module",
"exports": {
@@ -35,7 +35,9 @@
"@opencode-ai/util": "workspace:*",
"@shikijs/transformers": "3.9.2",
"@solid-primitives/active-element": "2.1.3",
"@solid-primitives/audio": "1.4.2",
"@solid-primitives/event-bus": "1.1.2",
"@solid-primitives/media": "2.3.3",
"@solid-primitives/resize-observer": "2.1.3",
"@solid-primitives/scroll": "2.1.3",
"@solid-primitives/storage": "4.3.3",
+22 -19
View File
@@ -14,6 +14,7 @@ import { LayoutProvider } from "./context/layout"
import { GlobalSDKProvider } from "./context/global-sdk"
import { SessionProvider } from "./context/session"
import { Show } from "solid-js"
import { NotificationProvider } from "./context/notification"
declare global {
interface Window {
@@ -37,25 +38,27 @@ export function App() {
<GlobalSDKProvider url={url}>
<GlobalSyncProvider>
<LayoutProvider>
<MetaProvider>
<Font />
<Router root={Layout}>
<Route path="/" component={Home} />
<Route path="/:dir" component={DirectoryLayout}>
<Route path="/" component={() => <Navigate href="session" />} />
<Route
path="/session/:id?"
component={(p) => (
<Show when={p.params.id || true} keyed>
<SessionProvider>
<Session />
</SessionProvider>
</Show>
)}
/>
</Route>
</Router>
</MetaProvider>
<NotificationProvider>
<MetaProvider>
<Font />
<Router root={Layout}>
<Route path="/" component={Home} />
<Route path="/:dir" component={DirectoryLayout}>
<Route path="/" component={() => <Navigate href="session" />} />
<Route
path="/session/:id?"
component={(p) => (
<Show when={p.params.id || true} keyed>
<SessionProvider>
<Session />
</SessionProvider>
</Show>
)}
/>
</Route>
</Router>
</MetaProvider>
</NotificationProvider>
</LayoutProvider>
</GlobalSyncProvider>
</GlobalSDKProvider>
+113
View File
@@ -0,0 +1,113 @@
import { useGlobalSync } from "@/context/global-sync"
import { useLayout } from "@/context/layout"
import { Session } from "@opencode-ai/sdk/v2/client"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { Mark } from "@opencode-ai/ui/logo"
import { Select } from "@opencode-ai/ui/select"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { base64Decode } from "@opencode-ai/util/encode"
import { getFilename } from "@opencode-ai/util/path"
import { A, useParams } from "@solidjs/router"
import { createMemo, Show } from "solid-js"
export function Header(props: {
navigateToProject: (directory: string) => void
navigateToSession: (session: Session | undefined) => void
}) {
const globalSync = useGlobalSync()
const layout = useLayout()
const params = useParams()
const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
const store = createMemo(() => globalSync.child(currentDirectory())[0])
const sessions = createMemo(() => store().session ?? [])
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
return (
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
<A
href="/"
classList={{
"w-12 shrink-0 px-4 py-3.5": true,
"flex items-center justify-start self-stretch": true,
"border-r border-border-weak-base": true,
}}
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
data-tauri-drag-region
>
<Mark class="shrink-0" />
</A>
<div class="pl-4 px-6 flex items-center justify-between gap-4 w-full">
<Show when={params.dir && layout.projects.list().length > 0}>
<div class="flex items-center gap-3">
<div class="flex items-center gap-2">
<Select
options={layout.projects.list().map((project) => project.worktree)}
current={currentDirectory()}
label={(x) => getFilename(x)}
onSelect={(x) => (x ? props.navigateToProject(x) : undefined)}
class="text-14-regular text-text-base"
variant="ghost"
>
{/* @ts-ignore */}
{(i) => (
<div class="flex items-center gap-2">
<Icon name="folder" size="small" />
<div class="text-text-strong">{getFilename(i)}</div>
</div>
)}
</Select>
<div class="text-text-weaker">/</div>
<Select
options={sessions()}
current={currentSession()}
placeholder="New session"
label={(x) => x.title}
value={(x) => x.id}
onSelect={props.navigateToSession}
class="text-14-regular text-text-base max-w-md"
variant="ghost"
/>
</div>
<Show when={currentSession()}>
<Button as={A} href={`/${params.dir}/session`} icon="plus-small">
New session
</Button>
</Show>
</div>
<div class="flex items-center gap-4">
<Tooltip
class="shrink-0"
value={
<div class="flex items-center gap-2">
<span>Toggle terminal</span>
<span class="text-icon-base text-12-medium">Ctrl `</span>
</div>
}
>
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
class="group-hover/terminal-toggle:hidden"
/>
<Icon
size="small"
name="layout-bottom-partial"
class="hidden group-hover/terminal-toggle:inline-block"
/>
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
class="hidden group-active/terminal-toggle:inline-block"
/>
</div>
</Button>
</Tooltip>
</div>
</Show>
</div>
</header>
)
}
+17
View File
@@ -0,0 +1,17 @@
import { ComponentProps, splitProps } from "solid-js"
import { usePlatform } from "@/context/platform"
export interface LinkProps extends ComponentProps<"button"> {
href: string
}
export function Link(props: LinkProps) {
const platform = usePlatform()
const [local, rest] = splitProps(props, ["href", "children"])
return (
<button class="text-text-strong underline" onClick={() => platform.openLink(local.href)} {...rest}>
{local.children}
</button>
)
}
+299 -116
View File
@@ -1,6 +1,7 @@
import { useFilteredList } from "@opencode-ai/ui/hooks"
import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Match, createSignal } from "solid-js"
import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Match, createMemo } from "solid-js"
import { createStore } from "solid-js/store"
import { makePersisted } from "@solid-primitives/storage"
import { createFocusSignal } from "@solid-primitives/active-element"
import { useLocal } from "@/context/local"
import { ContentPart, DEFAULT_PROMPT, isPromptEqual, Prompt, useSession } from "@/context/session"
@@ -21,7 +22,6 @@ import { popularProviders, useProviders } from "@/hooks/use-providers"
import { Dialog } from "@opencode-ai/ui/dialog"
import { List, ListRef } from "@opencode-ai/ui/list"
import { iife } from "@opencode-ai/util/iife"
import { Input } from "@opencode-ai/ui/input"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { IconName } from "@opencode-ai/ui/icons/provider"
@@ -70,22 +70,85 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const [store, setStore] = createStore<{
popoverIsOpen: boolean
historyIndex: number
savedPrompt: Prompt | null
placeholder: number
}>({
popoverIsOpen: false,
historyIndex: -1,
savedPrompt: null,
placeholder: Math.floor(Math.random() * PLACEHOLDERS.length),
})
const [placeholder, setPlaceholder] = createSignal(Math.floor(Math.random() * PLACEHOLDERS.length))
const MAX_HISTORY = 100
const [history, setHistory] = makePersisted(
createStore<{
entries: Prompt[]
}>({
entries: [],
}),
{
name: "prompt-history.v1",
},
)
onMount(() => {
const interval = setInterval(() => {
setPlaceholder((prev) => (prev + 1) % PLACEHOLDERS.length)
}, 6500)
onCleanup(() => clearInterval(interval))
})
const clonePromptParts = (prompt: Prompt): Prompt =>
prompt.map((part) =>
part.type === "text"
? { ...part }
: {
...part,
selection: part.selection ? { ...part.selection } : undefined,
},
)
const promptLength = (prompt: Prompt) => prompt.reduce((len, part) => len + part.content.length, 0)
const applyHistoryPrompt = (prompt: Prompt, position: "start" | "end") => {
const length = position === "start" ? 0 : promptLength(prompt)
session.prompt.set(prompt, length)
requestAnimationFrame(() => {
editorRef.focus()
setCursorPosition(editorRef, length)
})
}
const getCaretLineState = () => {
const selection = window.getSelection()
if (!selection || selection.rangeCount === 0) return { collapsed: false, onFirstLine: false, onLastLine: false }
const range = selection.getRangeAt(0)
const rect = range.getBoundingClientRect()
const editorRect = editorRef.getBoundingClientRect()
const style = window.getComputedStyle(editorRef)
const paddingTop = parseFloat(style.paddingTop) || 0
const paddingBottom = parseFloat(style.paddingBottom) || 0
let lineHeight = parseFloat(style.lineHeight)
if (!Number.isFinite(lineHeight)) lineHeight = parseFloat(style.fontSize) || 16
const scrollTop = editorRef.scrollTop
let relativeTop = rect.top - editorRect.top - paddingTop + scrollTop
if (!Number.isFinite(relativeTop)) relativeTop = scrollTop
relativeTop = Math.max(0, relativeTop)
let caretHeight = rect.height
if (!caretHeight || !Number.isFinite(caretHeight)) caretHeight = lineHeight
const relativeBottom = relativeTop + caretHeight
const contentHeight = Math.max(caretHeight, editorRef.scrollHeight - paddingTop - paddingBottom)
const threshold = Math.max(2, lineHeight / 2)
return {
collapsed: selection.isCollapsed,
onFirstLine: relativeTop <= threshold,
onLastLine: contentHeight - relativeBottom <= threshold,
}
}
createEffect(() => {
session.id
editorRef.focus()
if (session.id) return
const interval = setInterval(() => {
setStore("placeholder", (prev) => (prev + 1) % PLACEHOLDERS.length)
}, 6500)
onCleanup(() => clearInterval(interval))
})
const isFocused = createFocusSignal(() => editorRef)
@@ -118,17 +181,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
addPart({ type: "file", path, content: "@" + path, start: 0, end: 0 })
}
const { flat, active, onInput, onKeyDown, refetch } = useFilteredList<string>({
const { flat, active, onInput, onKeyDown } = useFilteredList<string>({
items: local.file.searchFilesAndDirectories,
key: (x) => x,
onSelect: handleFileSelect,
})
createEffect(() => {
local.model.recent()
refetch()
})
createEffect(
on(
() => session.prompt.current(),
@@ -210,6 +268,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
setStore("popoverIsOpen", false)
}
if (store.historyIndex >= 0) {
setStore("historyIndex", -1)
setStore("savedPrompt", null)
}
session.prompt.set(rawParts, cursorPosition)
}
@@ -285,12 +348,100 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
sessionID: session.id!,
})
const addToHistory = (prompt: Prompt) => {
const text = prompt
.map((p) => p.content)
.join("")
.trim()
if (!text) return
const entry = clonePromptParts(prompt)
const lastEntry = history.entries[0]
if (lastEntry) {
const lastText = lastEntry.map((p) => p.content).join("")
if (lastText === text) return
}
setHistory("entries", (entries) => [entry, ...entries].slice(0, MAX_HISTORY))
}
const navigateHistory = (direction: "up" | "down") => {
const entries = history.entries
const current = store.historyIndex
if (direction === "up") {
if (entries.length === 0) return false
if (current === -1) {
setStore("savedPrompt", clonePromptParts(session.prompt.current()))
setStore("historyIndex", 0)
applyHistoryPrompt(entries[0], "start")
return true
}
if (current < entries.length - 1) {
const next = current + 1
setStore("historyIndex", next)
applyHistoryPrompt(entries[next], "start")
return true
}
return false
}
if (current > 0) {
const next = current - 1
setStore("historyIndex", next)
applyHistoryPrompt(entries[next], "end")
return true
}
if (current === 0) {
setStore("historyIndex", -1)
const saved = store.savedPrompt
if (saved) {
applyHistoryPrompt(saved, "end")
setStore("savedPrompt", null)
return true
}
applyHistoryPrompt(DEFAULT_PROMPT, "end")
return true
}
return false
}
const handleKeyDown = (event: KeyboardEvent) => {
if (store.popoverIsOpen && (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter")) {
onKeyDown(event)
event.preventDefault()
return
}
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
const { collapsed, onFirstLine, onLastLine } = getCaretLineState()
if (!collapsed) return
const cursorPos = getCursorPosition(editorRef)
const textLength = promptLength(session.prompt.current())
const inHistory = store.historyIndex >= 0
const isStart = cursorPos === 0
const isEnd = cursorPos === textLength
const atAbsoluteStart = onFirstLine && isStart
const atAbsoluteEnd = onLastLine && isEnd
const allowUp = (inHistory && isEnd) || atAbsoluteStart
const allowDown = (inHistory && isStart) || atAbsoluteEnd
if (event.key === "ArrowUp") {
if (!allowUp) return
if (navigateHistory("up")) {
event.preventDefault()
}
return
}
if (!allowDown) return
if (navigateHistory("down")) {
event.preventDefault()
}
return
}
if (event.key === "Enter" && !event.shiftKey) {
handleSubmit(event)
}
@@ -312,6 +463,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
return
}
addToHistory(prompt)
setStore("historyIndex", -1)
setStore("savedPrompt", null)
let existing = session.info()
if (!existing) {
const created = await sdk.client.session.create()
@@ -450,7 +605,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
/>
<Show when={!session.prompt.dirty()}>
<div class="absolute top-0 left-0 px-5 py-3 text-14-regular text-text-weak pointer-events-none">
Ask anything... "{PLACEHOLDERS[placeholder()]}"
Ask anything... "{PLACEHOLDERS[store.placeholder]}"
</div>
</Show>
</div>
@@ -470,60 +625,74 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</Button>
<Show when={layout.dialog.opened() === "model"}>
<Switch>
<Match when={providers().connected().length > 0}>
<SelectDialog
defaultOpen
onOpenChange={(open) => {
if (open) {
layout.dialog.open("model")
} else {
layout.dialog.close("model")
}
}}
title="Select model"
placeholder="Search models"
emptyMessage="No model results"
key={(x) => `${x.provider.id}:${x.id}`}
items={local.model.list()}
current={local.model.current()}
filterKeys={["provider.name", "name", "id"]}
// groupBy={(x) => (local.model.recent().includes(x) ? "Recent" : x.provider.name)}
groupBy={(x) => x.provider.name}
sortGroupsBy={(a, b) => {
if (a.category === "Recent" && b.category !== "Recent") return -1
if (b.category === "Recent" && a.category !== "Recent") return 1
const aProvider = a.items[0].provider.id
const bProvider = b.items[0].provider.id
if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
}}
onSelect={(x) =>
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, { recent: true })
}
actions={
<Button
class="h-7 -my-1 text-14-medium"
icon="plus-small"
tabIndex={-1}
onClick={() => layout.dialog.open("provider")}
<Match when={providers.paid().length > 0}>
{iife(() => {
const models = createMemo(() =>
local.model
.list()
.filter((m) =>
layout.connect.state() === "complete" ? m.provider.id === layout.connect.provider() : true,
),
)
return (
<SelectDialog
defaultOpen
onOpenChange={(open) => {
if (open) {
layout.dialog.open("model")
} else {
layout.dialog.close("model")
}
}}
title="Select model"
placeholder="Search models"
emptyMessage="No model results"
key={(x) => `${x.provider.id}:${x.id}`}
items={models}
current={local.model.current()}
filterKeys={["provider.name", "name", "id"]}
sortBy={(a, b) => a.name.localeCompare(b.name)}
// groupBy={(x) => (local.model.recent().includes(x) ? "Recent" : x.provider.name)}
groupBy={(x) => x.provider.name}
sortGroupsBy={(a, b) => {
if (a.category === "Recent" && b.category !== "Recent") return -1
if (b.category === "Recent" && a.category !== "Recent") return 1
const aProvider = a.items[0].provider.id
const bProvider = b.items[0].provider.id
if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
}}
onSelect={(x) =>
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
recent: true,
})
}
actions={
<Button
class="h-7 -my-1 text-14-medium"
icon="plus-small"
tabIndex={-1}
onClick={() => layout.dialog.open("provider")}
>
Connect provider
</Button>
}
>
Connect provider
</Button>
}
>
{(i) => (
<div class="w-full flex items-center gap-x-2.5">
<span>{i.name}</span>
<Show when={!i.cost || i.cost?.input === 0}>
<Tag>Free</Tag>
</Show>
<Show when={i.latest}>
<Tag>Latest</Tag>
</Show>
</div>
)}
</SelectDialog>
{(i) => (
<div class="w-full flex items-center gap-x-2.5">
<span>{i.name}</span>
<Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}>
<Tag>Free</Tag>
</Show>
<Show when={i.latest}>
<Tag>Latest</Tag>
</Show>
</div>
)}
</SelectDialog>
)
})}
</Match>
<Match when={true}>
{iife(() => {
@@ -532,6 +701,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
if (e.key === "Escape") return
listRef?.onKeyDown(e)
}
onMount(() => {
document.addEventListener("keydown", handleKey)
onCleanup(() => {
document.removeEventListener("keydown", handleKey)
})
})
return (
<Dialog
modal
@@ -549,12 +726,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<Dialog.CloseButton tabIndex={-1} />
</Dialog.Header>
<Dialog.Body>
<Input hidden type="text" class="opacity-0 size-0" autofocus onKeyDown={handleKey} />
<div class="flex flex-col gap-3 px-2.5">
<div class="text-14-medium text-text-base px-2.5">Free models provided by OpenCode</div>
<List
ref={(ref) => (listRef = ref)}
items={local.model.list()}
items={local.model.list}
current={local.model.current()}
key={(x) => `${x.provider.id}:${x.id}`}
onSelect={(x) => {
@@ -579,54 +755,61 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</div>
<div class="px-1.5 pb-1.5">
<div class="w-full rounded-sm border border-border-weak-base bg-surface-raised-base">
<div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-6">
<div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-4">
<div class="px-2 text-14-medium text-text-base">
Add more models from popular providers
</div>
<List
class="w-full"
key={(x) => x?.id}
items={providers().popular()}
activeIcon="plus-small"
sortBy={(a, b) => {
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
return a.name.localeCompare(b.name)
}}
onSelect={(x) => {
layout.dialog.close("model")
}}
>
{(i) => (
<div class="w-full flex items-center gap-x-4">
<ProviderIcon
data-slot="list-item-extra-icon"
id={i.id as IconName}
// TODO: clean this up after we update icon in models.dev
classList={{
"text-icon-weak-base": true,
"size-4 mx-0.5": i.id === "opencode",
"size-5": i.id !== "opencode",
}}
/>
<span>{i.name}</span>
<Show when={i.id === "opencode"}>
<Tag>Recommended</Tag>
</Show>
<Show when={i.id === "anthropic"}>
<div class="text-14-regular text-text-weak">
Connect with Claude Pro/Max or API key
</div>
</Show>
</div>
)}
</List>
<Button variant="ghost" class="w-full justify-start">
<div class="flex items-center gap-2">
<Icon name="plus-small" />
<div class="text-text-strong">View all providers</div>
</div>
</Button>
<div class="w-full">
<List
class="w-full"
key={(x) => x?.id}
items={providers.popular}
activeIcon="plus-small"
sortBy={(a, b) => {
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
return a.name.localeCompare(b.name)
}}
onSelect={(x) => {
if (!x) return
layout.dialog.connect(x.id)
}}
>
{(i) => (
<div class="w-full flex items-center gap-x-4">
<ProviderIcon
data-slot="list-item-extra-icon"
id={i.id as IconName}
// TODO: clean this up after we update icon in models.dev
classList={{
"text-icon-weak-base": true,
"size-4 mx-0.5": i.id === "opencode",
"size-5": i.id !== "opencode",
}}
/>
<span>{i.name}</span>
<Show when={i.id === "opencode"}>
<Tag>Recommended</Tag>
</Show>
<Show when={i.id === "anthropic"}>
<div class="text-14-regular text-text-weak">
Connect with Claude Pro/Max or API key
</div>
</Show>
</div>
)}
</List>
<Button
variant="ghost"
class="w-full justify-start px-[11px] py-3.5 gap-4.5 text-14-medium"
icon="dot-grid"
onClick={() => {
layout.dialog.open("provider")
}}
>
View all providers
</Button>
</div>
</div>
</div>
</div>
+14 -5
View File
@@ -1,8 +1,9 @@
import { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
import { ComponentProps, onCleanup, onMount, splitProps } from "solid-js"
import { ComponentProps, createEffect, onCleanup, onMount, splitProps } from "solid-js"
import { useSDK } from "@/context/sdk"
import { SerializeAddon } from "@/addons/serialize"
import { LocalPTY } from "@/context/session"
import { usePrefersDark } from "@solid-primitives/media"
export interface TerminalProps extends ComponentProps<"div"> {
pty: LocalPTY
@@ -21,6 +22,7 @@ export const Terminal = (props: TerminalProps) => {
let serializeAddon: SerializeAddon
let fitAddon: FitAddon
let handleResize: () => void
const prefersDark = usePrefersDark()
onMount(async () => {
ghostty = await Ghostty.load()
@@ -31,10 +33,17 @@ export const Terminal = (props: TerminalProps) => {
fontSize: 14,
fontFamily: "TX-02, monospace",
allowTransparency: true,
theme: {
background: "#191515",
foreground: "#d4d4d4",
},
theme: prefersDark()
? {
background: "#191515",
foreground: "#d4d4d4",
cursor: "#d4d4d4",
}
: {
background: "#fcfcfc",
foreground: "#211e1e",
cursor: "#211e1e",
},
scrollback: 10_000,
ghostty,
})
+105 -30
View File
@@ -1,22 +1,25 @@
import type {
Message,
Agent,
Session,
Part,
Config,
Path,
File,
FileNode,
Project,
FileDiff,
Todo,
SessionStatus,
ProviderListResponse,
} from "@opencode-ai/sdk/v2"
import {
type Message,
type Agent,
type Session,
type Part,
type Config,
type Path,
type File,
type FileNode,
type Project,
type FileDiff,
type Todo,
type SessionStatus,
type ProviderListResponse,
type ProviderAuthResponse,
createOpencodeClient,
} from "@opencode-ai/sdk/v2/client"
import { createStore, produce, reconcile } from "solid-js/store"
import { Binary } from "@opencode-ai/util/binary"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSDK } from "./global-sdk"
import { onMount } from "solid-js"
type State = {
ready: boolean
@@ -49,16 +52,20 @@ type State = {
export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimpleContext({
name: "GlobalSync",
init: () => {
const sdk = useGlobalSDK()
const globalSDK = useGlobalSDK()
const [globalStore, setGlobalStore] = createStore<{
ready: boolean
path: Path
project: Project[]
provider: ProviderListResponse
provider_auth: ProviderAuthResponse
children: Record<string, State>
}>({
ready: false,
path: { state: "", config: "", worktree: "", directory: "", home: "" },
project: [],
provider: { all: [], connected: [], default: {} },
provider_auth: {},
children: {},
})
@@ -83,16 +90,53 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
changes: [],
})
children[directory] = createStore(globalStore.children[directory])
bootstrapInstance(directory)
}
return children[directory]
}
sdk.event.listen((e) => {
async function loadSessions(directory: string) {
globalSDK.client.session.list({ directory }).then((x) => {
const sessions = (x.data ?? [])
.slice()
.filter((s) => !s.time.archived)
.sort((a, b) => a.id.localeCompare(b.id))
.slice(0, 5)
const [, setStore] = child(directory)
setStore("session", sessions)
})
}
async function bootstrapInstance(directory: string) {
const [, setStore] = child(directory)
const sdk = createOpencodeClient({
baseUrl: globalSDK.url,
directory,
})
const load = {
project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
provider: () => sdk.provider.list().then((x) => setStore("provider", x.data!)),
path: () => sdk.path.get().then((x) => setStore("path", x.data!)),
agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
session: () => loadSessions(directory),
status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)),
config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
changes: () => sdk.file.status().then((x) => setStore("changes", x.data!)),
node: () => sdk.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
}
await Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
}
globalSDK.event.listen((e) => {
const directory = e.name
const event = e.details
if (directory === "global") {
switch (event.type) {
case "global.disposed": {
bootstrap()
break
}
case "project.updated": {
const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id)
if (result.found) {
@@ -113,8 +157,23 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
const [store, setStore] = child(directory)
switch (event.type) {
case "server.instance.disposed": {
bootstrapInstance(directory)
break
}
case "session.updated": {
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
if (event.properties.info.time.archived) {
if (result.found) {
setStore(
"session",
produce((draft) => {
draft.splice(result.index, 1)
}),
)
}
break
}
if (result.found) {
setStore("session", result.index, reconcile(event.properties.info))
break
@@ -181,19 +240,31 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
}
})
Promise.all([
sdk.client.project.list().then(async (x) => {
setGlobalStore(
"project",
x
.data!.filter((p) => !p.worktree.includes("opencode-test") && p.vcs)
.sort((a, b) => a.id.localeCompare(b.id)),
)
}),
sdk.client.provider.list().then((x) => {
setGlobalStore("provider", x.data ?? {})
}),
]).then(() => setGlobalStore("ready", true))
async function bootstrap() {
return Promise.all([
globalSDK.client.path.get().then((x) => {
setGlobalStore("path", x.data!)
}),
globalSDK.client.project.list().then(async (x) => {
setGlobalStore(
"project",
x
.data!.filter((p) => !p.worktree.includes("opencode-test") && p.vcs)
.sort((a, b) => a.id.localeCompare(b.id)),
)
}),
globalSDK.client.provider.list().then((x) => {
setGlobalStore("provider", x.data ?? {})
}),
globalSDK.client.provider.auth().then((x) => {
setGlobalStore("provider_auth", x.data ?? {})
}),
]).then(() => setGlobalStore("ready", true))
}
onMount(() => {
bootstrap()
})
return {
data: globalStore,
@@ -201,6 +272,10 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
return globalStore.ready
},
child,
bootstrap,
project: {
loadSessions,
},
}
},
})
+83 -40
View File
@@ -1,23 +1,28 @@
import { createStore } from "solid-js/store"
import { createMemo, onMount } from "solid-js"
import { createStore, produce } from "solid-js/store"
import { batch, createMemo, onMount } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { makePersisted } from "@solid-primitives/storage"
import { useGlobalSync } from "./global-sync"
import { useGlobalSDK } from "./global-sdk"
import { Project } from "@opencode-ai/sdk/v2"
const PASTEL_COLORS = [
"#FCEAFD", // pastel pink
"#FFDFBA", // pastel peach
"#FFFFBA", // pastel yellow
"#BAFFC9", // pastel green
"#EAF6FD", // pastel blue
"#EFEAFD", // pastel lavender
"#FEC8D8", // pastel rose
"#D4F0F0", // pastel cyan
"#FDF0EA", // pastel coral
"#C1E1C1", // pastel mint
]
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number]
export function getAvatarColors(key?: string) {
if (key && AVATAR_COLOR_KEYS.includes(key as AvatarColorKey)) {
return {
background: `var(--avatar-background-${key})`,
foreground: `var(--avatar-text-${key})`,
}
}
return {
background: "var(--surface-info-base)",
foreground: "var(--text-base)",
}
}
type Dialog = "provider" | "model" | "connect"
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
name: "Layout",
@@ -40,19 +45,27 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
}),
{
name: "default-layout.v7",
name: "layout.v1",
},
)
const [ephemeral, setEphemeral] = createStore({
const [ephemeral, setEphemeral] = createStore<{
connect: {
provider?: string
state?: "pending" | "complete" | "error"
error?: string
}
dialog: {
open: undefined as undefined | "provider" | "model",
},
open?: Dialog
}
}>({
connect: {},
dialog: {},
})
const usedColors = new Set<string>()
const usedColors = new Set<AvatarColorKey>()
function pickAvailableColor() {
const available = PASTEL_COLORS.filter((c) => !usedColors.has(c))
if (available.length === 0) return PASTEL_COLORS[Math.floor(Math.random() * PASTEL_COLORS.length)]
function pickAvailableColor(): AvatarColorKey {
const available = AVATAR_COLOR_KEYS.filter((c) => !usedColors.has(c))
if (available.length === 0) return AVATAR_COLOR_KEYS[Math.floor(Math.random() * AVATAR_COLOR_KEYS.length)]
return available[Math.floor(Math.random() * available.length)]
}
@@ -79,21 +92,10 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const enriched = createMemo(() => store.projects.flatMap(enrich))
const list = createMemo(() => enriched().flatMap(colorize))
async function loadProjectSessions(directory: string) {
const [, setStore] = globalSync.child(directory)
globalSdk.client.session.list({ directory }).then((x) => {
const sessions = (x.data ?? [])
.slice()
.sort((a, b) => a.id.localeCompare(b.id))
.slice(0, 5)
setStore("session", sessions)
})
}
onMount(() => {
Promise.all(
store.projects.map((project) => {
return loadProjectSessions(project.worktree)
return globalSync.project.loadSessions(project.worktree)
}),
)
})
@@ -103,7 +105,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
list,
open(directory: string) {
if (store.projects.find((x) => x.worktree === directory)) return
loadProjectSessions(directory)
globalSync.project.loadSessions(directory)
setStore("projects", (x) => [{ worktree: directory, expanded: true }, ...x])
},
close(directory: string) {
@@ -169,14 +171,55 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
dialog: {
opened: createMemo(() => ephemeral.dialog?.open),
open(dialog: "provider" | "model") {
setEphemeral("dialog", "open", dialog)
open(dialog: Dialog) {
batch(() => {
// if (dialog !== "connect") {
// setEphemeral("connect", {})
// }
setEphemeral("dialog", "open", dialog)
})
},
close(dialog: "provider" | "model") {
if (ephemeral.dialog?.open === dialog) {
setEphemeral("dialog", "open", undefined)
close(dialog: Dialog) {
if (ephemeral.dialog.open === dialog) {
setEphemeral(
produce((state) => {
state.dialog.open = undefined
state.connect = {}
}),
)
}
},
connect(provider: string) {
setEphemeral(
produce((state) => {
state.dialog.open = "connect"
state.connect = { provider, state: "pending" }
}),
)
},
},
connect: {
provider: createMemo(() => ephemeral.connect.provider),
state: createMemo(() => ephemeral.connect.state),
complete() {
setEphemeral(
produce((state) => {
state.dialog.open = "model"
state.connect.state = "complete"
}),
)
},
error(message: string) {
setEphemeral(
produce((state) => {
state.connect.state = "error"
state.connect.error = message
}),
)
},
clear() {
setEphemeral("connect", {})
},
},
}
},
+22 -16
View File
@@ -6,6 +6,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
import { useSDK } from "./sdk"
import { useSync } from "./sync"
import { base64Encode } from "@opencode-ai/util/encode"
import { useProviders } from "@/hooks/use-providers"
export type LocalFile = FileNode &
Partial<{
@@ -37,10 +38,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
init: () => {
const sdk = useSDK()
const sync = useSync()
const providers = useProviders()
function isModelValid(model: ModelKey) {
const provider = sync.data.provider?.all.find((x) => x.id === model.providerID)
return !!provider?.models[model.modelID] && sync.data.provider?.connected.includes(model.providerID)
const provider = providers.all().find((x) => x.id === model.providerID)
return (
!!provider?.models[model.modelID] &&
providers
.connected()
.map((p) => p.id)
.includes(model.providerID)
)
}
function getFirstValidModel(...modelFns: (() => ModelKey | undefined)[]) {
@@ -115,16 +123,14 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})
const list = createMemo(() =>
sync.data.provider.all
.filter((p) => sync.data.provider.connected.includes(p.id))
.flatMap((p) =>
Object.values(p.models).map((m) => ({
...m,
name: m.name.replace("(latest)", "").trim(),
provider: p,
latest: m.name.includes("(latest)"),
})),
),
providers.connected().flatMap((p) =>
Object.values(p.models).map((m) => ({
...m,
name: m.name.replace("(latest)", "").trim(),
provider: p,
latest: m.name.includes("(latest)"),
})),
),
)
const find = (key: ModelKey) => list().find((m) => m.id === key?.modelID && m.provider.id === key.providerID)
@@ -145,11 +151,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
}
for (const p of sync.data.provider.connected) {
if (p in sync.data.provider.default) {
for (const p of providers.connected()) {
if (p.id in providers.default()) {
return {
providerID: p,
modelID: sync.data.provider.default[p],
providerID: p.id,
modelID: providers.default()[p.id],
}
}
}
@@ -0,0 +1,106 @@
import { createStore } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { makePersisted } from "@solid-primitives/storage"
import { useGlobalSDK } from "./global-sdk"
import { EventSessionError } from "@opencode-ai/sdk/v2"
import { makeAudioPlayer } from "@solid-primitives/audio"
import idleSound from "@opencode-ai/ui/audio/staplebops-01.aac"
type NotificationBase = {
directory?: string
session?: string
metadata?: any
time: number
viewed: boolean
}
type TurnCompleteNotification = NotificationBase & {
type: "turn-complete"
}
type ErrorNotification = NotificationBase & {
type: "error"
error: EventSessionError["properties"]["error"]
}
export type Notification = TurnCompleteNotification | ErrorNotification
export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({
name: "Notification",
init: () => {
const idlePlayer = makeAudioPlayer(idleSound)
const globalSDK = useGlobalSDK()
const [store, setStore] = makePersisted(
createStore({
list: [] as Notification[],
}),
{
name: "notification.v1",
},
)
// onMount(() => {
// const daysToKeep = 7
// // setStore("list", (n) => n.filter((n) => !n.viewed && n.time + 1000 * 60 * 60 * 24 * daysToKeep < Date.now()))
// })
globalSDK.event.listen((e) => {
const directory = e.name
const event = e.details
const base = {
directory,
time: Date.now(),
viewed: false,
}
switch (event.type) {
case "session.idle": {
idlePlayer.play()
const session = event.properties.sessionID
setStore("list", store.list.length, {
...base,
type: "turn-complete",
session,
})
break
}
case "session.error": {
const session = event.properties.sessionID ?? "global"
// errorPlayer.play()
setStore("list", store.list.length, {
...base,
type: "error",
session,
error: "error" in event.properties ? event.properties.error : undefined,
})
break
}
}
})
return {
session: {
all(session: string) {
return store.list.filter((n) => n.session === session)
},
unseen(session: string) {
return store.list.filter((n) => n.session === session && !n.viewed)
},
markViewed(session: string) {
setStore("list", (n) => n.session === session, "viewed", true)
},
},
project: {
all(directory: string) {
return store.list.filter((n) => n.directory === directory)
},
unseen(directory: string) {
return store.list.filter((n) => n.directory === directory && !n.viewed)
},
markViewed(directory: string) {
setStore("list", (n) => n.directory === directory, "viewed", true)
},
},
}
},
})
+2 -2
View File
@@ -62,10 +62,10 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
const userMessages = createMemo(() =>
messages()
.filter((m) => m.role === "user")
.sort((a, b) => b.id.localeCompare(a.id)),
.sort((a, b) => a.id.localeCompare(b.id)),
)
const lastUserMessage = createMemo(() => {
return userMessages()?.at(0)
return userMessages()?.at(-1)
})
const activeMessage = createMemo(() => {
if (!store.messageId) return lastUserMessage()
+16 -24
View File
@@ -11,28 +11,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const globalSync = useGlobalSync()
const sdk = useSDK()
const [store, setStore] = globalSync.child(sdk.directory)
const load = {
project: () => sdk.client.project.current().then((x) => setStore("project", x.data!.id)),
provider: () => sdk.client.provider.list().then((x) => setStore("provider", x.data!)),
path: () => sdk.client.path.get().then((x) => setStore("path", x.data!)),
agent: () => sdk.client.app.agents().then((x) => setStore("agent", x.data ?? [])),
session: () =>
sdk.client.session.list().then((x) => {
const sessions = (x.data ?? [])
.slice()
.sort((a, b) => a.id.localeCompare(b.id))
.slice(0, store.limit)
setStore("session", sessions)
}),
status: () => sdk.client.session.status().then((x) => setStore("session_status", x.data!)),
config: () => sdk.client.config.get().then((x) => setStore("config", x.data!)),
changes: () => sdk.client.file.status().then((x) => setStore("changes", x.data!)),
node: () => sdk.client.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
}
Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
const absolute = (path: string) => (store.path.directory + "/" + path).replace("//", "/")
return {
@@ -78,11 +56,25 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
},
fetch: async (count = 10) => {
setStore("limit", (x) => x + count)
await load.session()
await sdk.client.session.list().then((x) => {
const sessions = (x.data ?? [])
.slice()
.sort((a, b) => a.id.localeCompare(b.id))
.slice(0, store.limit)
setStore("session", sessions)
})
},
more: createMemo(() => store.session.length >= store.limit),
archive: async (sessionID: string) => {
await sdk.client.session.update({ sessionID, time: { archived: Date.now() } })
setStore(
produce((draft) => {
const match = Binary.search(draft.session, sessionID, (s) => s.id)
if (match.found) draft.session.splice(match.index, 1)
}),
)
},
},
load,
absolute,
get directory() {
return store.path.directory
+8 -8
View File
@@ -16,16 +16,16 @@ export function useProviders() {
}
return globalSync.data.provider
})
const connected = createMemo(() =>
providers().all.filter(
(p) => providers().connected.includes(p.id) && Object.values(p.models).find((m) => m.cost?.input),
),
const connected = createMemo(() => providers().all.filter((p) => providers().connected.includes(p.id)))
const paid = createMemo(() =>
connected().filter((p) => p.id !== "opencode" || Object.values(p.models).find((m) => m.cost?.input)),
)
const popular = createMemo(() => providers().all.filter((p) => popularProviders.includes(p.id)))
return createMemo(() => ({
all: providers().all,
default: providers().default,
return {
all: createMemo(() => providers().all),
default: createMemo(() => providers().default),
popular,
connected,
}))
paid,
}
}
+3 -2
View File
@@ -1,5 +1,5 @@
import { useGlobalSync } from "@/context/global-sync"
import { For, Match, Show, Switch } from "solid-js"
import { createMemo, For, Match, Show, Switch } from "solid-js"
import { Button } from "@opencode-ai/ui/button"
import { Logo } from "@opencode-ai/ui/logo"
import { useLayout } from "@/context/layout"
@@ -14,6 +14,7 @@ export default function Home() {
const layout = useLayout()
const platform = usePlatform()
const navigate = useNavigate()
const homedir = createMemo(() => sync.data.path.home)
function openProject(directory: string) {
layout.projects.open(directory)
@@ -61,7 +62,7 @@ export default function Home() {
class="text-14-mono text-left justify-between px-3"
onClick={() => openProject(project.worktree)}
>
{project.worktree}
{project.worktree.replace(homedir(), "~")}
<div class="text-14-regular text-text-weak">
{DateTime.fromMillis(project.time.updated ?? project.time.created).toRelative()}
</div>
+598 -179
View File
@@ -1,10 +1,21 @@
import { createEffect, createMemo, For, Match, ParentProps, Show, Switch, type JSX } from "solid-js"
import {
createEffect,
createMemo,
createSignal,
For,
Match,
onCleanup,
onMount,
ParentProps,
Show,
Switch,
type JSX,
} from "solid-js"
import { DateTime } from "luxon"
import { A, useNavigate, useParams } from "@solidjs/router"
import { useLayout } from "@/context/layout"
import { useLayout, getAvatarColors } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync"
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
import { Mark } from "@opencode-ai/ui/logo"
import { Avatar } from "@opencode-ai/ui/avatar"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Button } from "@opencode-ai/ui/button"
@@ -15,11 +26,10 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Collapsible } from "@opencode-ai/ui/collapsible"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { getFilename } from "@opencode-ai/util/path"
import { Select } from "@opencode-ai/ui/select"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Session, Project } from "@opencode-ai/sdk/v2/client"
import { Session, Project, ProviderAuthMethod, ProviderAuthAuthorization } from "@opencode-ai/sdk/v2/client"
import { usePlatform } from "@/context/platform"
import { createStore } from "solid-js/store"
import { createStore, produce } from "solid-js/store"
import {
DragDropProvider,
DragDropSensors,
@@ -34,6 +44,17 @@ import { SelectDialog } from "@opencode-ai/ui/select-dialog"
import { Tag } from "@opencode-ai/ui/tag"
import { IconName } from "@opencode-ai/ui/icons/provider"
import { popularProviders, useProviders } from "@/hooks/use-providers"
import { Dialog } from "@opencode-ai/ui/dialog"
import { iife } from "@opencode-ai/util/iife"
import { Link } from "@/components/link"
import { List, ListRef } from "@opencode-ai/ui/list"
import { TextField } from "@opencode-ai/ui/text-field"
import { showToast, Toast } from "@opencode-ai/ui/toast"
import { useGlobalSDK } from "@/context/global-sdk"
import { Spinner } from "@opencode-ai/ui/spinner"
import { useNotification } from "@/context/notification"
import { Binary } from "@opencode-ai/util/binary"
import { Header } from "@/components/header"
export default function Layout(props: ParentProps) {
const [store, setStore] = createStore({
@@ -42,13 +63,12 @@ export default function Layout(props: ParentProps) {
})
const params = useParams()
const globalSDK = useGlobalSDK()
const globalSync = useGlobalSync()
const layout = useLayout()
const platform = usePlatform()
const notification = useNotification()
const navigate = useNavigate()
const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
const sessions = createMemo(() => globalSync.child(currentDirectory())[0].session ?? [])
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
const providers = useProviders()
function navigateToProject(directory: string | undefined) {
@@ -68,9 +88,11 @@ export default function Layout(props: ParentProps) {
}
function closeProject(directory: string) {
const index = layout.projects.list().findIndex((x) => x.worktree === directory)
const next = layout.projects.list()[index + 1]
layout.projects.close(directory)
// TODO: more intelligent navigation
navigate("/")
if (next) navigateToProject(next.worktree)
else navigate("/")
}
async function chooseProject() {
@@ -96,6 +118,7 @@ export default function Layout(props: ParentProps) {
if (!params.dir || !params.id) return
const directory = base64Decode(params.dir)
setStore("lastSession", directory, params.id)
notification.session.markViewed(params.id)
})
createEffect(() => {
@@ -155,8 +178,53 @@ export default function Layout(props: ParentProps) {
return <></>
}
const ProjectAvatar = (props: {
project: Project
class?: string
expandable?: boolean
notify?: boolean
}): JSX.Element => {
const notification = useNotification()
const notifications = createMemo(() => notification.project.unseen(props.project.worktree))
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
const name = createMemo(() => getFilename(props.project.worktree))
const mask = "radial-gradient(circle 5px at calc(100% - 2px) 2px, transparent 5px, black 5.5px)"
const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
return (
<div class="relative size-6 shrink-0">
<Avatar
fallback={name()}
src={props.project.id === opencode ? "https://opencode.ai/favicon.svg" : props.project.icon?.url}
{...getAvatarColors(props.project.icon?.color)}
class={`size-full ${props.class ?? ""}`}
style={
notifications().length > 0 && props.notify ? { "-webkit-mask-image": mask, "mask-image": mask } : undefined
}
/>
<Show when={props.expandable}>
<Icon
name="chevron-right"
size="large"
class="hidden size-full items-center justify-center text-text-subtle group-hover/session:flex group-data-[expanded]/trigger:rotate-90 transition-transform duration-50"
/>
</Show>
<Show when={notifications().length > 0 && props.notify}>
<div
classList={{
"absolute -top-0.5 -right-0.5 size-1.5 rounded-full": true,
"bg-icon-critical-base": hasError(),
"bg-text-interactive-base": !hasError(),
}}
/>
</Show>
</div>
)
}
const ProjectVisual = (props: { project: Project & { expanded: boolean }; class?: string }): JSX.Element => {
const name = createMemo(() => getFilename(props.project.worktree))
const current = createMemo(() => base64Decode(params.dir ?? ""))
return (
<Switch>
<Match when={layout.sidebar.opened()}>
@@ -167,14 +235,7 @@ export default function Layout(props: ParentProps) {
class="flex items-center justify-between gap-3 w-full px-1 self-stretch h-8 border-none rounded-lg"
>
<div class="flex items-center gap-3 p-0 text-left min-w-0 grow">
<div class="size-6 shrink-0">
<Avatar
fallback={name()}
src={props.project.icon?.url}
background={props.project.icon?.color ?? "var(--surface-info-base)"}
class="size-full"
/>
</div>
<ProjectAvatar project={props.project} />
<span class="truncate text-14-medium text-text-strong">{name()}</span>
</div>
</Button>
@@ -184,17 +245,10 @@ export default function Layout(props: ParentProps) {
variant="ghost"
size="large"
class="flex items-center justify-center p-0 aspect-square border-none rounded-lg"
data-selected={props.project.worktree === currentDirectory()}
data-selected={props.project.worktree === current()}
onClick={() => navigateToProject(props.project.worktree)}
>
<div class="size-6 shrink-0">
<Avatar
fallback={name()}
src={props.project.icon?.url}
background={props.project.icon?.color ?? "var(--surface-info-base)"}
class="size-full"
/>
</div>
<ProjectAvatar project={props.project} notify />
</Button>
</Match>
</Switch>
@@ -202,35 +256,31 @@ export default function Layout(props: ParentProps) {
}
const SortableProject = (props: { project: Project & { expanded: boolean } }): JSX.Element => {
const notification = useNotification()
const sortable = createSortable(props.project.worktree)
const [projectStore] = globalSync.child(props.project.worktree)
const slug = createMemo(() => base64Encode(props.project.worktree))
const name = createMemo(() => getFilename(props.project.worktree))
const [store, setStore] = globalSync.child(props.project.worktree)
const sessions = createMemo(() => store.session ?? [])
const [expanded, setExpanded] = createSignal(true)
return (
// @ts-ignore
<div use:sortable classList={{ "opacity-30": sortable.isActiveDraggable }}>
<Switch>
<Match when={layout.sidebar.opened()}>
<Collapsible variant="ghost" defaultOpen class="gap-2 shrink-0">
<Collapsible variant="ghost" defaultOpen class="gap-2 shrink-0" onOpenChange={setExpanded}>
<Button
as={"div"}
variant="ghost"
class="group/session flex items-center justify-between gap-3 w-full px-1 self-stretch h-auto border-none rounded-lg"
>
<Collapsible.Trigger class="group/trigger flex items-center gap-3 p-0 text-left min-w-0 grow border-none">
<div class="size-6 shrink-0">
<Avatar
fallback={name()}
src={props.project.icon?.url}
background={props.project.icon?.color ?? "var(--surface-info-base)"}
class="size-full group-hover/session:hidden"
/>
<Icon
name="chevron-right"
size="large"
class="hidden size-full items-center justify-center text-text-subtle group-hover/session:flex group-data-[expanded]/trigger:rotate-90 transition-transform duration-50"
/>
</div>
<ProjectAvatar
project={props.project}
class="group-hover/session:hidden"
expandable
notify={!expanded()}
/>
<span class="truncate text-14-medium text-text-strong">{name()}</span>
</Collapsible.Trigger>
<div class="flex invisible gap-1 items-center group-hover/session:visible has-[[data-expanded]]:visible">
@@ -251,50 +301,105 @@ export default function Layout(props: ParentProps) {
</Button>
<Collapsible.Content>
<nav class="hidden @[4rem]:flex w-full flex-col gap-1.5">
<For each={projectStore.session}>
<For each={sessions()}>
{(session) => {
const updated = createMemo(() => DateTime.fromMillis(session.time.updated))
const notifications = createMemo(() => notification.session.unseen(session.id))
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
async function archive(session: Session) {
await globalSDK.client.session.update({
directory: session.directory,
sessionID: session.id,
time: { archived: Date.now() },
})
setStore(
produce((draft) => {
const match = Binary.search(draft.session, session.id, (s) => s.id)
if (match.found) draft.session.splice(match.index, 1)
}),
)
}
return (
<A
data-active={session.id === params.id}
href={`${slug()}/session/${session.id}`}
class="group/session focus:outline-none cursor-default"
<div
class="group/session relative w-full pl-4 pr-1 py-1 rounded-md cursor-default transition-colors
hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover"
>
<Tooltip placement="right" value={session.title}>
<div
class="w-full pl-4 pr-2 py-1 rounded-md
group-data-[active=true]/session:bg-surface-raised-base-hover
group-hover/session:bg-surface-raised-base-hover
group-focus/session:bg-surface-raised-base-hover"
<Tooltip placement="right" value={session.title} gutter={10}>
<A
href={`${slug()}/session/${session.id}`}
class="flex flex-col min-w-0 text-left w-full focus:outline-none"
>
<div class="flex items-center self-stretch gap-6 justify-between">
<div class="flex items-center self-stretch gap-6 justify-between transition-[padding] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7">
<span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
{session.title}
</span>
<span class="text-12-regular text-text-weak text-right whitespace-nowrap">
{Math.abs(updated().diffNow().as("seconds")) < 60
? "Now"
: updated()
.toRelative({
style: "short",
unit: ["days", "hours", "minutes"],
})
?.replace(" ago", "")
?.replace(/ days?/, "d")
?.replace(" min.", "m")
?.replace(" hr.", "h")}
</span>
<div class="shrink-0 group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
<Switch>
<Match when={hasError()}>
<div class="size-1.5 mr-1.5 rounded-full bg-text-diff-delete-base" />
</Match>
<Match when={notifications().length > 0}>
<div class="size-1.5 mr-1.5 rounded-full bg-text-interactive-base" />
</Match>
<Match when={true}>
<span class="text-12-regular text-text-weak text-right whitespace-nowrap">
{Math.abs(updated().diffNow().as("seconds")) < 60
? "Now"
: updated()
.toRelative({
style: "short",
unit: ["days", "hours", "minutes"],
})
?.replace(" ago", "")
?.replace(/ days?/, "d")
?.replace(" min.", "m")
?.replace(" hr.", "h")}
</span>
</Match>
</Switch>
</div>
</div>
<div class="hidden _flex justify-between items-center self-stretch">
<span class="text-12-regular text-text-weak">{`${session.summary?.files || "No"} file${session.summary?.files !== 1 ? "s" : ""} changed`}</span>
<Show when={session.summary}>{(summary) => <DiffChanges changes={summary()} />}</Show>
</div>
</div>
<Show when={session.summary?.files}>
<div class="flex justify-between items-center self-stretch">
<span class="text-12-regular text-text-weak">{`${session.summary?.files || "No"} file${session.summary?.files !== 1 ? "s" : ""} changed`}</span>
<Show when={session.summary}>{(summary) => <DiffChanges changes={summary()} />}</Show>
</div>
</Show>
</A>
</Tooltip>
</A>
<div class="hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute top-1 right-1">
{/* <IconButton icon="dot-grid" variant="ghost" /> */}
<Tooltip placement="right" value="Archive session">
<IconButton icon="archive" variant="ghost" onClick={() => archive(session)} />
</Tooltip>
</div>
</div>
)
}}
</For>
<Show when={sessions().length === 0}>
<div
class="group/session relative w-full pl-4 pr-1 py-1 rounded-md cursor-default transition-colors
hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover"
>
<div class="flex items-center self-stretch w-full">
<div class="flex-1 min-w-0">
<Tooltip placement="right" value="New session">
<A
href={`${slug()}/session`}
class="flex flex-col gap-1 min-w-0 text-left w-full focus:outline-none"
>
<div class="flex items-center self-stretch gap-6 justify-between">
<span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
New session
</span>
</div>
</A>
</Tooltip>
</div>
</div>
</div>
</Show>
</nav>
</Collapsible.Content>
</Collapsible>
@@ -323,93 +428,9 @@ export default function Layout(props: ParentProps) {
}
return (
<div class="relative h-screen flex flex-col">
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
<A
href="/"
classList={{
"w-12 shrink-0 px-4 py-3.5": true,
"flex items-center justify-start self-stretch": true,
"border-r border-border-weak-base": true,
}}
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
data-tauri-drag-region
>
<Mark class="shrink-0" />
</A>
<div class="pl-4 px-6 flex items-center justify-between gap-4 w-full">
<Show when={params.dir && layout.projects.list().length > 0}>
<div class="flex items-center gap-3">
<div class="flex items-center gap-2">
<Select
options={layout.projects.list().map((project) => project.worktree)}
current={currentDirectory()}
label={(x) => getFilename(x)}
onSelect={(x) => (x ? navigateToProject(x) : undefined)}
class="text-14-regular text-text-base"
variant="ghost"
>
{/* @ts-ignore */}
{(i) => (
<div class="flex items-center gap-2">
<Icon name="folder" size="small" />
<div class="text-text-strong">{getFilename(i)}</div>
</div>
)}
</Select>
<div class="text-text-weaker">/</div>
<Select
options={sessions()}
current={currentSession()}
placeholder="New session"
label={(x) => x.title}
value={(x) => x.id}
onSelect={navigateToSession}
class="text-14-regular text-text-base max-w-md"
variant="ghost"
/>
</div>
<Show when={currentSession()}>
<Button as={A} href={`/${params.dir}/session`} icon="plus-small">
New session
</Button>
</Show>
</div>
<div class="flex items-center gap-4">
<Tooltip
class="shrink-0"
value={
<div class="flex items-center gap-2">
<span>Toggle terminal</span>
<span class="text-icon-base text-12-medium">Ctrl `</span>
</div>
}
>
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
class="group-hover/terminal-toggle:hidden"
/>
<Icon
size="small"
name="layout-bottom-partial"
class="hidden group-hover/terminal-toggle:inline-block"
/>
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
class="hidden group-active/terminal-toggle:inline-block"
/>
</div>
</Button>
</Tooltip>
</div>
</Show>
</div>
</header>
<div class="h-[calc(100vh-3rem)] flex">
<div class="relative flex-1 min-h-0 flex flex-col">
<Header navigateToProject={navigateToProject} navigateToSession={navigateToSession} />
<div class="flex-1 min-h-0 flex">
<div
classList={{
"relative @container w-12 pb-5 shrink-0 bg-background-base": true,
@@ -434,7 +455,7 @@ export default function Layout(props: ParentProps) {
<Button
variant="ghost"
size="large"
class="group/sidebar-toggle shrink-0 w-full text-left justify-start rounded-lg"
class="group/sidebar-toggle shrink-0 w-full text-left justify-start rounded-lg px-2"
onClick={layout.sidebar.toggle}
>
<div class="relative -ml-px flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
@@ -481,7 +502,7 @@ export default function Layout(props: ParentProps) {
</div>
<div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
<Switch>
<Match when={!providers().connected().length && layout.sidebar.opened()}>
<Match when={!providers.paid().length && layout.sidebar.opened()}>
<div class="rounded-md bg-background-stronger shadow-xs-border-base">
<div class="p-3 flex flex-col gap-2">
<div class="text-12-medium text-text-strong">Getting started</div>
@@ -492,7 +513,7 @@ export default function Layout(props: ParentProps) {
<Button
class="flex w-full text-left justify-start text-12-medium text-text-strong stroke-[1.5px] rounded-lg rounded-t-none shadow-none border-t border-border-weak-base pl-2.25 pb-px"
size="large"
icon="plus-small"
icon="plus"
onClick={connectProvider}
>
<Show when={layout.sidebar.opened()}>Connect provider</Show>
@@ -506,7 +527,7 @@ export default function Layout(props: ParentProps) {
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg px-2"
variant="ghost"
size="large"
icon="plus-small"
icon="plus"
onClick={connectProvider}
>
<Show when={layout.sidebar.opened()}>Connect provider</Show>
@@ -527,17 +548,17 @@ export default function Layout(props: ParentProps) {
</Button>
</Tooltip>
</Show>
<Tooltip placement="right" value="Settings" inactive={layout.sidebar.opened()}>
<Button
disabled
class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg px-2"
variant="ghost"
size="large"
icon="settings-gear"
>
<Show when={layout.sidebar.opened()}>Settings</Show>
</Button>
</Tooltip>
{/* <Tooltip placement="right" value="Settings" inactive={layout.sidebar.opened()}> */}
{/* <Button */}
{/* disabled */}
{/* class="flex w-full text-left justify-start text-12-medium text-text-base stroke-[1.5px] rounded-lg px-2" */}
{/* variant="ghost" */}
{/* size="large" */}
{/* icon="settings-gear" */}
{/* > */}
{/* <Show when={layout.sidebar.opened()}>Settings</Show> */}
{/* </Button> */}
{/* </Tooltip> */}
<Tooltip placement="right" value="Share feedback" inactive={layout.sidebar.opened()}>
<Button
as={"a"}
@@ -561,8 +582,7 @@ export default function Layout(props: ParentProps) {
placeholder="Search providers"
activeIcon="plus-small"
key={(x) => x?.id}
items={providers().all}
// current={local.model.current()}
items={providers.all}
filterKeys={["id", "name"]}
groupBy={(x) => (popularProviders.includes(x.id) ? "Popular" : "Other")}
sortBy={(a, b) => {
@@ -575,7 +595,10 @@ export default function Layout(props: ParentProps) {
if (b.category === "Popular" && a.category !== "Popular") return 1
return 0
}}
// onSelect={(x) => }
onSelect={(x) => {
if (!x) return
layout.dialog.connect(x.id)
}}
onOpenChange={(open) => {
if (open) {
layout.dialog.open("provider")
@@ -607,7 +630,403 @@ export default function Layout(props: ParentProps) {
)}
</SelectDialog>
</Show>
<Show when={layout.dialog.opened() === "connect"}>
{iife(() => {
const providerID = createMemo(() => layout.connect.provider()!)
const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === providerID())!)
const methods = createMemo(
() =>
globalSync.data.provider_auth[providerID()] ?? [
{
type: "api",
label: "API key",
},
],
)
const [store, setStore] = createStore({
method: undefined as undefined | ProviderAuthMethod,
authorization: undefined as undefined | ProviderAuthAuthorization,
state: "pending" as undefined | "pending" | "complete" | "error",
error: undefined as string | undefined,
})
const methodIndex = createMemo(() => methods().findIndex((x) => x.label === store.method?.label))
async function selectMethod(index: number) {
const method = methods()[index]
setStore(
produce((draft) => {
draft.method = method
draft.authorization = undefined
draft.state = undefined
draft.error = undefined
}),
)
if (method.type === "oauth") {
setStore("state", "pending")
const start = Date.now()
await globalSDK.client.provider.oauth
.authorize(
{
providerID: providerID(),
method: index,
},
{ throwOnError: true },
)
.then((x) => {
const elapsed = Date.now() - start
const delay = 1000 - elapsed
if (delay > 0) {
setTimeout(() => {
setStore("state", "complete")
setStore("authorization", x.data!)
}, delay)
return
}
setStore("state", "complete")
setStore("authorization", x.data!)
})
.catch((e) => {
setStore("state", "error")
setStore("error", String(e))
})
}
}
let listRef: ListRef | undefined
function handleKey(e: KeyboardEvent) {
if (e.key === "Enter" && e.target instanceof HTMLInputElement) {
return
}
if (e.key === "Escape") return
listRef?.onKeyDown(e)
}
onMount(() => {
if (methods().length === 1) {
selectMethod(0)
}
document.addEventListener("keydown", handleKey)
onCleanup(() => {
document.removeEventListener("keydown", handleKey)
})
})
async function complete() {
await globalSDK.client.global.dispose()
setTimeout(() => {
showToast({
variant: "success",
icon: "circle-check",
title: `${provider().name} connected`,
description: `${provider().name} models are now available to use.`,
})
layout.connect.complete()
}, 500)
}
return (
<Dialog
modal
defaultOpen
onOpenChange={(open) => {
if (open) {
layout.dialog.open("connect")
} else {
layout.dialog.close("connect")
}
}}
>
<Dialog.Header class="px-4.5">
<Dialog.Title class="flex items-center">
<IconButton
tabIndex={-1}
icon="arrow-left"
variant="ghost"
onClick={() => {
if (methods().length === 1) {
layout.dialog.open("provider")
return
}
if (store.authorization) {
setStore("authorization", undefined)
setStore("method", undefined)
return
}
if (store.method) {
setStore("method", undefined)
return
}
layout.dialog.open("provider")
}}
/>
</Dialog.Title>
<Dialog.CloseButton tabIndex={-1} />
</Dialog.Header>
<Dialog.Body>
<div class="flex flex-col gap-6 px-2.5 pb-3">
<div class="px-2.5 flex gap-4 items-center">
<ProviderIcon id={providerID() as IconName} class="size-5 shrink-0 icon-strong-base" />
<div class="text-16-medium text-text-strong">
<Switch>
<Match
when={providerID() === "anthropic" && store.method?.label?.toLowerCase().includes("max")}
>
Login with Claude Pro/Max
</Match>
<Match when={true}>Connect {provider().name}</Match>
</Switch>
</div>
</div>
<div class="px-2.5 pb-10 flex flex-col gap-6">
<Switch>
<Match when={store.method === undefined}>
<div class="text-14-regular text-text-base">Select login method for {provider().name}.</div>
<div class="">
<List
ref={(ref) => (listRef = ref)}
items={methods}
key={(m) => m?.label}
onSelect={async (method, index) => {
if (!method) return
selectMethod(index)
}}
>
{(i) => (
<div class="w-full flex items-center gap-x-4">
<div class="w-4 h-2 rounded-[1px] bg-input-base shadow-xs-border-base flex items-center justify-center">
<div
class="w-2.5 h-0.5 bg-icon-strong-base hidden"
data-slot="list-item-extra-icon"
/>
</div>
<span>{i.label}</span>
</div>
)}
</List>
</div>
</Match>
<Match when={store.state === "pending"}>
<div class="text-14-regular text-text-base">
<div class="flex items-center gap-x-4">
<Spinner />
<span>Authorization in progress...</span>
</div>
</div>
</Match>
<Match when={store.state === "error"}>
<div class="text-14-regular text-text-base">
<div class="flex items-center gap-x-4">
<Icon name="circle-ban-sign" class="text-icon-critical-base" />
<span>Authorization failed: {store.error}</span>
</div>
</div>
</Match>
<Match when={store.method?.type === "api"}>
{iife(() => {
const [formStore, setFormStore] = createStore({
value: "",
error: undefined as string | undefined,
})
async function handleSubmit(e: SubmitEvent) {
e.preventDefault()
const form = e.currentTarget as HTMLFormElement
const formData = new FormData(form)
const apiKey = formData.get("apiKey") as string
if (!apiKey?.trim()) {
setFormStore("error", "API key is required")
return
}
setFormStore("error", undefined)
await globalSDK.client.auth.set({
providerID: providerID(),
auth: {
type: "api",
key: apiKey,
},
})
await complete()
}
return (
<div class="flex flex-col gap-6">
<Switch>
<Match when={provider().id === "opencode"}>
<div class="flex flex-col gap-4">
<div class="text-14-regular text-text-base">
OpenCode Zen gives you access to a curated set of reliable optimized models for
coding agents.
</div>
<div class="text-14-regular text-text-base">
With a single API key youll get access to models such as Claude, GPT, Gemini,
GLM and more.
</div>
<div class="text-14-regular text-text-base">
Visit{" "}
<Link href="https://opencode.ai/zen" tabIndex={-1}>
opencode.ai/zen
</Link>{" "}
to collect your API key.
</div>
</div>
</Match>
<Match when={true}>
<div class="text-14-regular text-text-base">
Enter your {provider().name} API key to connect your account and use{" "}
{provider().name} models in OpenCode.
</div>
</Match>
</Switch>
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
<TextField
autofocus
type="text"
label={`${provider().name} API key`}
placeholder="API key"
name="apiKey"
value={formStore.value}
onChange={setFormStore.bind(null, "value")}
validationState={formStore.error ? "invalid" : undefined}
error={formStore.error}
/>
<Button class="w-auto" type="submit" size="large" variant="primary">
Submit
</Button>
</form>
</div>
)
})}
</Match>
<Match when={store.method?.type === "oauth"}>
<Switch>
<Match when={store.authorization?.method === "code"}>
{iife(() => {
const [formStore, setFormStore] = createStore({
value: "",
error: undefined as string | undefined,
})
onMount(() => {
if (store.authorization?.method === "code" && store.authorization?.url) {
platform.openLink(store.authorization.url)
}
})
async function handleSubmit(e: SubmitEvent) {
e.preventDefault()
const form = e.currentTarget as HTMLFormElement
const formData = new FormData(form)
const code = formData.get("code") as string
if (!code?.trim()) {
setFormStore("error", "Authorization code is required")
return
}
setFormStore("error", undefined)
const { error } = await globalSDK.client.provider.oauth.callback({
providerID: providerID(),
method: methodIndex(),
code,
})
if (!error) {
await complete()
return
}
setFormStore("error", "Invalid authorization code")
}
return (
<div class="flex flex-col gap-6">
<div class="text-14-regular text-text-base">
Visit <Link href={store.authorization!.url}>this link</Link> to collect your
authorization code to connect your account and use {provider().name} models in
OpenCode.
</div>
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
<TextField
autofocus
type="text"
label={`${store.method?.label} authorization code`}
placeholder="Authorization code"
name="code"
value={formStore.value}
onChange={setFormStore.bind(null, "value")}
validationState={formStore.error ? "invalid" : undefined}
error={formStore.error}
/>
<Button class="w-auto" type="submit" size="large" variant="primary">
Submit
</Button>
</form>
</div>
)
})}
</Match>
<Match when={store.authorization?.method === "auto"}>
{iife(() => {
const code = createMemo(() => {
const instructions = store.authorization?.instructions
if (instructions?.includes(":")) {
return instructions?.split(":")[1]?.trim()
}
return instructions
})
onMount(async () => {
const result = await globalSDK.client.provider.oauth.callback({
providerID: providerID(),
method: methodIndex(),
})
if (result.error) {
// TODO: show error
layout.dialog.close("connect")
return
}
await complete()
})
return (
<div class="flex flex-col gap-6">
<div class="text-14-regular text-text-base">
Visit <Link href={store.authorization!.url}>this link</Link> and enter the code
below to connect your account and use {provider().name} models in OpenCode.
</div>
<TextField
label="Confirmation code"
class="font-mono"
value={code()}
readOnly
copyable
/>
<div class="text-14-regular text-text-base flex items-center gap-4">
<Spinner />
<span>Waiting for authorization...</span>
</div>
</div>
)
})}
</Match>
</Switch>
</Match>
</Switch>
</div>
</div>
</Dialog.Body>
</Dialog>
)
})}
</Show>
</div>
<Toast.Region />
</div>
)
}
+1 -2
View File
@@ -415,7 +415,6 @@ export default function Page() {
messages={session.messages.user()}
current={session.messages.active()}
onMessageSelect={session.messages.setActive}
working={session.working()}
wide={wide()}
/>
<SessionTurn
@@ -676,7 +675,7 @@ export default function Page() {
<For each={session.terminal.all()}>{(terminal) => <SortableTerminalTab terminal={terminal} />}</For>
</SortableProvider>
<div class="h-full flex items-center justify-center">
<Tooltip value="Open file" class="flex items-center">
<Tooltip value="New Terminal" class="flex items-center">
<IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={session.terminal.new} />
</Tooltip>
</div>
+1 -1
View File
@@ -14,7 +14,7 @@
"strict": true,
"noEmit": false,
"emitDeclarationOnly": true,
"outDir": "ts-dist",
"outDir": "node_modules/.ts-dist",
"isolatedModules": true,
"paths": {
"@/*": ["./src/*"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.0.145",
"version": "1.0.153",
"private": true,
"type": "module",
"scripts": {
@@ -138,10 +138,18 @@ const getData = query(async (shareID) => {
export default function () {
const params = useParams()
const data = createAsync(async () => {
if (!params.shareID) throw new Error("Missing shareID")
return getData(params.shareID)
})
const data = createAsync(
async () => {
if (!params.shareID) throw new Error("Missing shareID")
const now = Date.now()
const data = getData(params.shareID)
console.log("getData", Date.now() - now)
return data
},
{
deferStream: true,
},
)
createEffect(() => {
console.log(data())
@@ -201,7 +209,7 @@ export default function () {
const messages = createMemo(() =>
data().sessionID
? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort(
(a, b) => b.time.created - a.time.created,
(a, b) => a.time.created - b.time.created,
)
: [],
)
+8 -4
View File
@@ -50,10 +50,6 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"Enterprise": {
"type": "sst.cloudflare.SolidStart"
"url": string
}
"GITHUB_APP_ID": {
"type": "sst.sst.Secret"
"value": string
@@ -94,6 +90,10 @@ declare module "sst" {
"type": "sst.sst.Linkable"
"value": string
}
"Teams": {
"type": "sst.cloudflare.SolidStart"
"url": string
}
"Web": {
"type": "sst.cloudflare.Astro"
"url": string
@@ -114,6 +114,10 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS5": {
"type": "sst.sst.Secret"
"value": string
}
}
}
// cloudflare
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.0.145"
version = "1.0.153"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/sst/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.145/opencode-darwin-arm64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.153/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.145/opencode-darwin-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.153/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.145/opencode-linux-arm64.tar.gz"
archive = "https://github.com/sst/opencode/releases/download/v1.0.153/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.145/opencode-linux-x64.tar.gz"
archive = "https://github.com/sst/opencode/releases/download/v1.0.153/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.145/opencode-windows-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.153/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.0.145",
"version": "1.0.153",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+8 -4
View File
@@ -50,10 +50,6 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"Enterprise": {
"type": "sst.cloudflare.SolidStart"
"url": string
}
"GITHUB_APP_ID": {
"type": "sst.sst.Secret"
"value": string
@@ -94,6 +90,10 @@ declare module "sst" {
"type": "sst.sst.Linkable"
"value": string
}
"Teams": {
"type": "sst.cloudflare.SolidStart"
"url": string
}
"Web": {
"type": "sst.cloudflare.Astro"
"url": string
@@ -114,6 +114,10 @@ declare module "sst" {
"type": "sst.sst.Secret"
"value": string
}
"ZEN_MODELS5": {
"type": "sst.sst.Secret"
"value": string
}
}
}
// cloudflare
+11 -3
View File
@@ -1,10 +1,18 @@
FROM alpine
FROM alpine AS base
# Disable the runtime transpiler cache by default inside Docker containers.
# On ephemeral containers, the cache is not useful
ARG BUN_RUNTIME_TRANSPILER_CACHE_PATH=0
ENV BUN_RUNTIME_TRANSPILER_CACHE_PATH=${BUN_RUNTIME_TRANSPILER_CACHE_PATH}
RUN apk add libgcc libstdc++
ADD ./dist/opencode-linux-x64-baseline-musl/bin/opencode /usr/local/bin/opencode
RUN apk add libgcc libstdc++ ripgrep
FROM base AS build-amd64
COPY dist/opencode-linux-x64-baseline-musl/bin/opencode /usr/local/bin/opencode
FROM base AS build-arm64
COPY dist/opencode-linux-arm64-musl/bin/opencode /usr/local/bin/opencode
ARG TARGETARCH
FROM build-${TARGETARCH}
RUN opencode --version
ENTRYPOINT ["opencode"]
+3 -3
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.0.145",
"version": "1.0.153",
"name": "opencode",
"type": "module",
"private": true,
@@ -71,8 +71,8 @@
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "1.5.2",
"@opentui/core": "0.1.60",
"@opentui/solid": "0.1.60",
"@opentui/core": "0.0.0-20251211-4403a69a",
"@opentui/solid": "0.0.0-20251211-4403a69a",
"@parcel/watcher": "2.5.1",
"@pierre/precision-diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
+3
View File
@@ -117,6 +117,9 @@ for (const item of targets) {
compile: {
autoloadBunfig: false,
autoloadDotenv: false,
//@ts-ignore (bun types aren't up to date)
autoloadTsconfig: true,
autoloadPackageJson: true,
target: name.replace(pkg.name, "bun") as any,
outfile: `dist/${name}/bin/opencode`,
execArgv: [`--user-agent=opencode/${Script.version}`, "--"],
+4 -4
View File
@@ -244,8 +244,8 @@ if (!Script.preview) {
await $`cd ./dist/homebrew-tap && git push`
const image = "ghcr.io/sst/opencode"
await $`docker build -t ${image}:${Script.version} .`
await $`docker push ${image}:${Script.version}`
await $`docker tag ${image}:${Script.version} ${image}:latest`
await $`docker push ${image}:latest`
const platforms = "linux/amd64,linux/arm64"
const tags = [`${image}:${Script.version}`, `${image}:latest`]
const tagFlags = tags.flatMap((t) => ["-t", t])
await $`docker buildx build --platform ${platforms} ${tagFlags} --push .`
}
+240 -2
View File
@@ -28,7 +28,7 @@ import { Config } from "@/config/config"
import { Todo } from "@/session/todo"
import { z } from "zod"
import { LoadAPIKeyError } from "ai"
import type { OpencodeClient } from "@opencode-ai/sdk/v2"
import type { OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2"
export namespace ACP {
const log = Log.create({ service: "acp-agent" })
@@ -386,7 +386,7 @@ export namespace ACP {
log.info("creating_session", { sessionId, mcpServers: params.mcpServers.length })
const load = await this.loadSession({
const load = await this.loadSessionMode({
cwd: directory,
mcpServers: params.mcpServers,
sessionId,
@@ -412,6 +412,242 @@ export namespace ACP {
}
async loadSession(params: LoadSessionRequest) {
const directory = params.cwd
const sessionId = params.sessionId
try {
const model = await defaultModel(this.config, directory)
// Store ACP session state
const state = await this.sessionManager.load(sessionId, params.cwd, params.mcpServers, model)
log.info("load_session", { sessionId, mcpServers: params.mcpServers.length })
const mode = await this.loadSessionMode({
cwd: directory,
mcpServers: params.mcpServers,
sessionId,
})
this.setupEventSubscriptions(state)
// Replay session history
const messages = await this.sdk.session
.messages(
{
sessionID: sessionId,
directory,
},
{ throwOnError: true },
)
.then((x) => x.data)
.catch((err) => {
log.error("unexpected error when fetching message", { error: err })
return undefined
})
for (const msg of messages ?? []) {
log.debug("replay message", msg)
await this.processMessage(msg)
}
return mode
} catch (e) {
const error = MessageV2.fromError(e, {
providerID: this.config.defaultModel?.providerID ?? "unknown",
})
if (LoadAPIKeyError.isInstance(error)) {
throw RequestError.authRequired()
}
throw e
}
}
private async processMessage(message: SessionMessageResponse) {
log.debug("process message", message)
if (message.info.role !== "assistant" && message.info.role !== "user") return
const sessionId = message.info.sessionID
for (const part of message.parts) {
if (part.type === "tool") {
switch (part.state.status) {
case "pending":
await this.connection
.sessionUpdate({
sessionId,
update: {
sessionUpdate: "tool_call",
toolCallId: part.callID,
title: part.tool,
kind: toToolKind(part.tool),
status: "pending",
locations: [],
rawInput: {},
},
})
.catch((err) => {
log.error("failed to send tool pending to ACP", { error: err })
})
break
case "running":
await this.connection
.sessionUpdate({
sessionId,
update: {
sessionUpdate: "tool_call_update",
toolCallId: part.callID,
status: "in_progress",
locations: toLocations(part.tool, part.state.input),
rawInput: part.state.input,
},
})
.catch((err) => {
log.error("failed to send tool in_progress to ACP", { error: err })
})
break
case "completed":
const kind = toToolKind(part.tool)
const content: ToolCallContent[] = [
{
type: "content",
content: {
type: "text",
text: part.state.output,
},
},
]
if (kind === "edit") {
const input = part.state.input
const filePath = typeof input["filePath"] === "string" ? input["filePath"] : ""
const oldText = typeof input["oldString"] === "string" ? input["oldString"] : ""
const newText =
typeof input["newString"] === "string"
? input["newString"]
: typeof input["content"] === "string"
? input["content"]
: ""
content.push({
type: "diff",
path: filePath,
oldText,
newText,
})
}
if (part.tool === "todowrite") {
const parsedTodos = z.array(Todo.Info).safeParse(JSON.parse(part.state.output))
if (parsedTodos.success) {
await this.connection
.sessionUpdate({
sessionId,
update: {
sessionUpdate: "plan",
entries: parsedTodos.data.map((todo) => {
const status: PlanEntry["status"] =
todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"])
return {
priority: "medium",
status,
content: todo.content,
}
}),
},
})
.catch((err) => {
log.error("failed to send session update for todo", { error: err })
})
} else {
log.error("failed to parse todo output", { error: parsedTodos.error })
}
}
await this.connection
.sessionUpdate({
sessionId,
update: {
sessionUpdate: "tool_call_update",
toolCallId: part.callID,
status: "completed",
kind,
content,
title: part.state.title,
rawOutput: {
output: part.state.output,
metadata: part.state.metadata,
},
},
})
.catch((err) => {
log.error("failed to send tool completed to ACP", { error: err })
})
break
case "error":
await this.connection
.sessionUpdate({
sessionId,
update: {
sessionUpdate: "tool_call_update",
toolCallId: part.callID,
status: "failed",
content: [
{
type: "content",
content: {
type: "text",
text: part.state.error,
},
},
],
rawOutput: {
error: part.state.error,
},
},
})
.catch((err) => {
log.error("failed to send tool error to ACP", { error: err })
})
break
}
} else if (part.type === "text") {
if (part.text) {
await this.connection
.sessionUpdate({
sessionId,
update: {
sessionUpdate: message.info.role === "user" ? "user_message_chunk" : "agent_message_chunk",
content: {
type: "text",
text: part.text,
},
},
})
.catch((err) => {
log.error("failed to send text to ACP", { error: err })
})
}
} else if (part.type === "reasoning") {
if (part.text) {
await this.connection
.sessionUpdate({
sessionId,
update: {
sessionUpdate: "agent_thought_chunk",
content: {
type: "text",
text: part.text,
},
},
})
.catch((err) => {
log.error("failed to send reasoning to ACP", { error: err })
})
}
}
}
}
private async loadSessionMode(params: LoadSessionRequest) {
const directory = params.cwd
const model = await defaultModel(this.config, directory)
const sessionId = params.sessionId
@@ -678,6 +914,8 @@ export namespace ACP {
{
sessionID,
directory,
providerID: model.providerID,
modelID: model.modelID,
},
{ throwOnError: true },
)
+31
View File
@@ -40,6 +40,37 @@ export class ACPSessionManager {
return state
}
async load(
sessionId: string,
cwd: string,
mcpServers: McpServer[],
model?: ACPSessionState["model"],
): Promise<ACPSessionState> {
const session = await this.sdk.session
.get(
{
sessionID: sessionId,
directory: cwd,
},
{ throwOnError: true },
)
.then((x) => x.data!)
const resolvedModel = model
const state: ACPSessionState = {
id: sessionId,
cwd,
mcpServers,
createdAt: new Date(session.time.created),
model: resolvedModel,
}
log.info("loading_session", { state })
this.sessions.set(sessionId, state)
return state
}
get(sessionId: string): ACPSessionState {
const session = this.sessions.get(sessionId)
if (!session) {
+10 -41
View File
@@ -85,47 +85,16 @@ export namespace BunProc {
version,
})
const total = 3
const wait = 500
const runInstall = async (count: number = 1): Promise<void> => {
log.info("bun install attempt", {
pkg,
version,
attempt: count,
total,
})
await BunProc.run(args, {
cwd: Global.Path.cache,
}).catch(async (error) => {
log.warn("bun install failed", {
pkg,
version,
attempt: count,
total,
error,
})
if (count >= total) {
throw new InstallFailedError(
{ pkg, version },
{
cause: error,
},
)
}
const delay = wait * count
log.info("bun install retrying", {
pkg,
version,
next: count + 1,
delay,
})
await Bun.sleep(delay)
return runInstall(count + 1)
})
}
await runInstall()
await BunProc.run(args, {
cwd: Global.Path.cache,
}).catch((e) => {
throw new InstallFailedError(
{ pkg, version },
{
cause: e,
},
)
})
// Resolve actual version from installed package when using "latest"
// This ensures subsequent starts use the cached version until explicitly updated
@@ -17,6 +17,7 @@ const DiagnosticsCommand = cmd({
async handler(args) {
await bootstrap(process.cwd(), async () => {
await LSP.touchFile(args.file, true)
await Bun.sleep(1000)
process.stdout.write(JSON.stringify(await LSP.diagnostics(), null, 2) + EOL)
})
},
+37 -11
View File
@@ -124,6 +124,8 @@ type IssueQueryResponse = {
}
}
const AGENT_USERNAME = "opencode-agent[bot]"
const AGENT_REACTION = "eyes"
const WORKFLOW_FILE = ".github/workflows/opencode.yml"
export const GithubCommand = cmd({
@@ -409,20 +411,33 @@ export const GithubRunCommand = cmd({
let exitCode = 0
type PromptFiles = Awaited<ReturnType<typeof getUserPrompt>>["promptFiles"]
const triggerCommentId = payload.comment.id
const useGithubToken = normalizeUseGithubToken()
try {
const actionToken = isMock ? args.token! : await getOidcToken()
appToken = await exchangeForAppToken(actionToken)
if (useGithubToken) {
const githubToken = process.env["GITHUB_TOKEN"]
if (!githubToken) {
throw new Error(
"GITHUB_TOKEN environment variable is not set. When using use_github_token, you must provide GITHUB_TOKEN.",
)
}
appToken = githubToken
} else {
const actionToken = isMock ? args.token! : await getOidcToken()
appToken = await exchangeForAppToken(actionToken)
}
octoRest = new Octokit({ auth: appToken })
octoGraph = graphql.defaults({
headers: { authorization: `token ${appToken}` },
})
const { userPrompt, promptFiles } = await getUserPrompt()
await configureGit(appToken)
if (!useGithubToken) {
await configureGit(appToken)
}
await assertPermissions()
await addReaction("eyes")
await addReaction()
// Setup opencode session
const repoData = await fetchRepo()
@@ -512,8 +527,10 @@ export const GithubRunCommand = cmd({
// Also output the clean error message for the action to capture
//core.setOutput("prepare_error", e.message);
} finally {
await restoreGitConfig()
await revokeAppToken()
if (!useGithubToken) {
await restoreGitConfig()
await revokeAppToken()
}
}
process.exit(exitCode)
@@ -542,6 +559,14 @@ export const GithubRunCommand = cmd({
throw new Error(`Invalid share value: ${value}. Share must be a boolean.`)
}
function normalizeUseGithubToken() {
const value = process.env["USE_GITHUB_TOKEN"]
if (!value) return false
if (value === "true") return true
if (value === "false") return false
throw new Error(`Invalid use_github_token value: ${value}. Must be a boolean.`)
}
function isIssueCommentEvent(
event: IssueCommentEvent | PullRequestReviewCommentEvent,
): event is IssueCommentEvent {
@@ -812,8 +837,8 @@ export const GithubRunCommand = cmd({
await $`git config --local --unset-all ${config}`
await $`git config --local ${config} "AUTHORIZATION: basic ${newCredentials}"`
await $`git config --global user.name "opencode-agent[bot]"`
await $`git config --global user.email "opencode-agent[bot]@users.noreply.github.com"`
await $`git config --global user.name "${AGENT_USERNAME}"`
await $`git config --global user.email "${AGENT_USERNAME}@users.noreply.github.com"`
}
async function restoreGitConfig() {
@@ -935,13 +960,13 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
if (!["admin", "write"].includes(permission)) throw new Error(`User ${actor} does not have write permissions`)
}
async function addReaction(reaction: "eyes") {
async function addReaction() {
console.log("Adding reaction...")
return await octoRest.rest.reactions.createForIssueComment({
owner,
repo,
comment_id: triggerCommentId,
content: reaction,
content: AGENT_REACTION,
})
}
@@ -951,9 +976,10 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
owner,
repo,
comment_id: triggerCommentId,
content: AGENT_REACTION,
})
const eyesReaction = reactions.data.find((r) => r.content === "eyes")
const eyesReaction = reactions.data.find((r) => r.user?.login === AGENT_USERNAME)
if (!eyesReaction) return
await octoRest.rest.reactions.deleteForIssueComment({
+37 -17
View File
@@ -107,7 +107,9 @@ export function tui(input: { url: string; args: Args; onExit?: () => Promise<voi
render(
() => {
return (
<ErrorBoundary fallback={(error, reset) => <ErrorComponent error={error} reset={reset} onExit={onExit} />}>
<ErrorBoundary
fallback={(error, reset) => <ErrorComponent error={error} reset={reset} onExit={onExit} mode={mode} />}
>
<ArgsProvider {...input.args}>
<ExitProvider onExit={onExit}>
<KVProvider>
@@ -173,14 +175,14 @@ function App() {
// Update terminal window title based on current route and session
createEffect(() => {
if (route.data.type === "home") {
renderer.setTerminalTitle("opencode")
renderer.setTerminalTitle("OpenCode")
return
}
if (route.data.type === "session") {
const session = sync.session.get(route.data.sessionID)
if (!session || SessionApi.isDefaultTitle(session.title)) {
renderer.setTerminalTitle("opencode")
renderer.setTerminalTitle("OpenCode")
return
}
@@ -216,7 +218,7 @@ function App() {
let continued = false
createEffect(() => {
if (continued || sync.status !== "complete" || !args.continue) return
const match = sync.data.session.at(0)?.id
const match = sync.data.session.find((x) => x.parentID === undefined)?.id
if (match) {
continued = true
route.navigate({ type: "session", sessionID: match })
@@ -536,7 +538,12 @@ function App() {
)
}
function ErrorComponent(props: { error: Error; reset: () => void; onExit: () => Promise<void> }) {
function ErrorComponent(props: {
error: Error
reset: () => void
onExit: () => Promise<void>
mode?: "dark" | "light"
}) {
const term = useTerminalDimensions()
useKeyboard((evt) => {
if (evt.ctrl && evt.name === "c") {
@@ -547,6 +554,15 @@ function ErrorComponent(props: { error: Error; reset: () => void; onExit: () =>
const issueURL = new URL("https://github.com/sst/opencode/issues/new?template=bug-report.yml")
// Choose safe fallback colors per mode since theme context may not be available
const isLight = props.mode === "light"
const colors = {
bg: isLight ? "#ffffff" : "#0a0a0a",
text: isLight ? "#1a1a1a" : "#eeeeee",
muted: isLight ? "#8a8a8a" : "#808080",
primary: isLight ? "#3b7dd8" : "#fab283",
}
if (props.error.message) {
issueURL.searchParams.set("title", `opentui: fatal: ${props.error.message}`)
}
@@ -567,27 +583,31 @@ function ErrorComponent(props: { error: Error; reset: () => void; onExit: () =>
}
return (
<box flexDirection="column" gap={1}>
<box flexDirection="column" gap={1} backgroundColor={colors.bg}>
<box flexDirection="row" gap={1} alignItems="center">
<text attributes={TextAttributes.BOLD}>Please report an issue.</text>
<box onMouseUp={copyIssueURL} backgroundColor="#565f89" padding={1}>
<text attributes={TextAttributes.BOLD}>Copy issue URL (exception info pre-filled)</text>
<text attributes={TextAttributes.BOLD} fg={colors.text}>
Please report an issue.
</text>
<box onMouseUp={copyIssueURL} backgroundColor={colors.primary} padding={1}>
<text attributes={TextAttributes.BOLD} fg={colors.bg}>
Copy issue URL (exception info pre-filled)
</text>
</box>
{copied() && <text>Successfully copied</text>}
{copied() && <text fg={colors.muted}>Successfully copied</text>}
</box>
<box flexDirection="row" gap={2} alignItems="center">
<text>A fatal error occurred!</text>
<box onMouseUp={props.reset} backgroundColor="#565f89" padding={1}>
<text>Reset TUI</text>
<text fg={colors.text}>A fatal error occurred!</text>
<box onMouseUp={props.reset} backgroundColor={colors.primary} padding={1}>
<text fg={colors.bg}>Reset TUI</text>
</box>
<box onMouseUp={props.onExit} backgroundColor="#565f89" padding={1}>
<text>Exit</text>
<box onMouseUp={props.onExit} backgroundColor={colors.primary} padding={1}>
<text fg={colors.bg}>Exit</text>
</box>
</box>
<scrollbox height={Math.floor(term().height * 0.7)}>
<text>{props.error.stack}</text>
<text fg={colors.muted}>{props.error.stack}</text>
</scrollbox>
<text>{props.error.message}</text>
<text fg={colors.text}>{props.error.message}</text>
</box>
)
}
+6 -1
View File
@@ -14,12 +14,17 @@ export const AttachCommand = cmd({
.option("dir", {
type: "string",
description: "directory to run in",
})
.option("session", {
alias: ["s"],
type: "string",
describe: "session id to continue",
}),
handler: async (args) => {
if (args.dir) process.chdir(args.dir)
await tui({
url: args.url,
args: {},
args: { sessionID: args.session },
})
},
})
@@ -199,7 +199,7 @@ export function DialogModel(props: { providerID?: string }) {
<DialogSelect
keybind={[
{
keybind: { ctrl: true, name: "a", meta: false, shift: false, leader: false },
keybind: Keybind.parse("ctrl+a")[0],
title: connected() ? "Connect provider" : "View all providers",
onTrigger() {
dialog.replace(() => <DialogProvider />)
@@ -122,7 +122,9 @@ function AutoMethod(props: AutoMethodProps) {
return (
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD}>{props.title}</text>
<text attributes={TextAttributes.BOLD} fg={theme.text}>
{props.title}
</text>
<text fg={theme.textMuted}>esc</text>
</box>
<box gap={1}>
@@ -198,7 +200,7 @@ function ApiMethod(props: ApiMethodProps) {
<text fg={theme.textMuted}>
OpenCode Zen gives you access to all the best coding models at the cheapest prices with a single API key.
</text>
<text>
<text fg={theme.text}>
Go to <span style={{ fg: theme.primary }}>https://opencode.ai/zen</span> to get a key
</text>
</box>
@@ -8,6 +8,7 @@ import { Keybind } from "@/util/keybind"
import { useTheme } from "../context/theme"
import { useSDK } from "../context/sdk"
import { DialogSessionRename } from "./dialog-session-rename"
import "opentui-spinner/solid"
export function DialogSessionList() {
const dialog = useDialog()
@@ -22,6 +23,8 @@ export function DialogSessionList() {
const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
const options = createMemo(() => {
const today = new Date().toDateString()
return sync.data.session
@@ -34,12 +37,15 @@ export function DialogSessionList() {
category = "Today"
}
const isDeleting = toDelete() === x.id
const status = sync.data.session_status[x.id]
const isWorking = status?.type === "busy"
return {
title: isDeleting ? `Press ${deleteKeybind} again to confirm` : x.title,
bg: isDeleting ? theme.error : undefined,
value: x.id,
category,
footer: Locale.time(x.time.updated),
gutter: isWorking ? <spinner frames={spinnerFrames} interval={80} color={theme.primary} /> : undefined,
}
})
.slice(0, 150)
@@ -19,7 +19,7 @@ export function DialogStatus() {
</text>
<text fg={theme.textMuted}>esc</text>
</box>
<Show when={Object.keys(sync.data.mcp).length > 0} fallback={<text>No MCP Servers</text>}>
<Show when={Object.keys(sync.data.mcp).length > 0} fallback={<text fg={theme.text}>No MCP Servers</text>}>
<box>
<text fg={theme.text}>{Object.keys(sync.data.mcp).length} MCP Servers</text>
<For each={Object.entries(sync.data.mcp)}>
@@ -1,4 +1,3 @@
import { Installation } from "@/installation"
import { TextAttributes } from "@opentui/core"
import { For } from "solid-js"
import { useTheme } from "@tui/context/theme"
@@ -14,8 +13,10 @@ export function Logo() {
<For each={LOGO_LEFT}>
{(line, index) => (
<box flexDirection="row" gap={1}>
<text fg={theme.textMuted}>{line}</text>
<text fg={theme.text} attributes={TextAttributes.BOLD}>
<text fg={theme.textMuted} selectable={false}>
{line}
</text>
<text fg={theme.text} attributes={TextAttributes.BOLD} selectable={false}>
{LOGO_RIGHT[index()]}
</text>
</box>
@@ -10,6 +10,7 @@ import { useSync } from "@tui/context/sync"
import { Identifier } from "@/id/id"
import { createStore, produce } from "solid-js/store"
import { useKeybind } from "@tui/context/keybind"
import { Keybind } from "@/util/keybind"
import { usePromptHistory, type PromptInfo } from "./history"
import { type AutocompleteRef, Autocomplete } from "./autocomplete"
import { useCommandDialog } from "../dialog-command"
@@ -24,6 +25,7 @@ import { Locale } from "@/util/locale"
import { createColors, createFrames } from "../../ui/spinner.ts"
import { useDialog } from "@tui/ui/dialog"
import { DialogProvider as DialogProviderConnect } from "../dialog-provider"
import { DialogAlert } from "../../ui/dialog-alert"
import { useToast } from "../../ui/toast"
export type PromptProps = {
@@ -46,6 +48,61 @@ export type PromptRef = {
const PLACEHOLDERS = ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"]
const TEXTAREA_ACTIONS = [
"submit",
"newline",
"move-left",
"move-right",
"move-up",
"move-down",
"select-left",
"select-right",
"select-up",
"select-down",
"line-home",
"line-end",
"select-line-home",
"select-line-end",
"visual-line-home",
"visual-line-end",
"select-visual-line-home",
"select-visual-line-end",
"buffer-home",
"buffer-end",
"select-buffer-home",
"select-buffer-end",
"delete-line",
"delete-to-line-end",
"delete-to-line-start",
"backspace",
"delete",
"undo",
"redo",
"word-forward",
"word-backward",
"select-word-forward",
"select-word-backward",
"delete-word-forward",
"delete-word-backward",
] as const
function mapTextareaKeybindings(
keybinds: Record<string, Keybind.Info[]>,
action: (typeof TEXTAREA_ACTIONS)[number],
): KeyBinding[] {
const configKey = `input_${action.replace(/-/g, "_")}`
const bindings = keybinds[configKey]
if (!bindings) return []
return bindings.map((binding) => ({
name: binding.name,
ctrl: binding.ctrl || undefined,
meta: binding.meta || undefined,
shift: binding.shift || undefined,
super: binding.super || undefined,
action,
}))
}
export function Prompt(props: PromptProps) {
let input: TextareaRenderable
let anchor: BoxRenderable
@@ -76,26 +133,12 @@ export function Prompt(props: PromptProps) {
}
const textareaKeybindings = createMemo(() => {
const newlineBindings = keybind.all.input_newline || []
const submitBindings = keybind.all.input_submit || []
const keybinds = keybind.all
return [
{ name: "return", action: "submit" },
{ name: "return", meta: true, action: "newline" },
...newlineBindings.map((binding) => ({
name: binding.name,
ctrl: binding.ctrl || undefined,
meta: binding.meta || undefined,
shift: binding.shift || undefined,
action: "newline" as const,
})),
...submitBindings.map((binding) => ({
name: binding.name,
ctrl: binding.ctrl || undefined,
meta: binding.meta || undefined,
shift: binding.shift || undefined,
action: "submit" as const,
})),
...TEXTAREA_ACTIONS.flatMap((action) => mapTextareaKeybindings(keybinds, action)),
] satisfies KeyBinding[]
})
@@ -662,8 +705,8 @@ export function Prompt(props: PromptProps) {
>
<textarea
placeholder={props.sessionID ? undefined : `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
textColor={theme.text}
focusedTextColor={theme.text}
textColor={keybind.leader ? theme.textMuted : theme.text}
focusedTextColor={keybind.leader ? theme.textMuted : theme.text}
minHeight={1}
maxHeight={6}
onContentChange={() => {
@@ -689,8 +732,12 @@ export function Prompt(props: PromptProps) {
return
}
if (keybind.match("app_exit", e)) {
await exit()
return
if (store.prompt.input === "") {
await exit()
// Don't preventDefault - let textarea potentially handle the event
e.preventDefault()
return
}
}
if (e.name === "!" && input.visualCursor.offset === 0) {
setStore("mode", "shell")
@@ -807,7 +854,7 @@ export function Prompt(props: PromptProps) {
</text>
<Show when={store.mode === "normal"}>
<box flexDirection="row" gap={1}>
<text flexShrink={0} fg={theme.text}>
<text flexShrink={0} fg={keybind.leader ? theme.textMuted : theme.text}>
{local.model.parsed().model}
</text>
<text fg={theme.textMuted}>{local.model.parsed().provider}</text>
@@ -822,25 +869,17 @@ export function Prompt(props: PromptProps) {
borderColor={highlight()}
customBorderChars={{
...EmptyBorder,
// when the background is transparent, don't draw the vertical line
vertical: theme.background.a != 0 ? "╹" : " ",
vertical: "╹",
}}
>
<box
height={1}
border={["bottom"]}
borderColor={theme.backgroundElement}
customBorderChars={
theme.background.a != 0
? {
...EmptyBorder,
horizontal: "▀",
}
: {
...EmptyBorder,
horizontal: " ",
}
}
customBorderChars={{
...EmptyBorder,
horizontal: "▀",
}}
/>
</box>
<box flexDirection="row" justifyContent="space-between">
@@ -866,9 +905,14 @@ export function Prompt(props: PromptProps) {
if (!r) return
if (r.message.includes("exceeded your current quota") && r.message.includes("gemini"))
return "gemini is way too hot right now"
if (r.message.length > 50) return r.message.slice(0, 50) + "..."
if (r.message.length > 80) return r.message.slice(0, 80) + "..."
return r.message
})
const isTruncated = createMemo(() => {
const r = retry()
if (!r) return false
return r.message.length > 120
})
const [seconds, setSeconds] = createSignal(0)
onMount(() => {
const timer = setInterval(() => {
@@ -880,12 +924,28 @@ export function Prompt(props: PromptProps) {
clearInterval(timer)
})
})
const handleMessageClick = () => {
const r = retry()
if (!r) return
if (isTruncated()) {
DialogAlert.show(dialog, "Retry Error", r.message)
}
}
const retryText = () => {
const r = retry()
if (!r) return ""
const baseMessage = message()
const truncatedHint = isTruncated() ? " (click to expand)" : ""
const retryInfo = ` [retrying ${seconds() > 0 ? `in ${seconds()}s ` : ""}attempt #${r.attempt}]`
return baseMessage + truncatedHint + retryInfo
}
return (
<Show when={retry()}>
<text fg={theme.error}>
{message()} [retrying {seconds() > 0 ? `in ${seconds()}s ` : ""}
attempt #{retry()!.attempt}]
</text>
<box onMouseUp={handleMessageClick}>
<text fg={theme.error}>{retryText()}</text>
</box>
</Show>
)
})()}
@@ -73,21 +73,11 @@ export const { use: useKeybind, provider: KeybindProvider } = createSimpleContex
return store.leader
},
parse(evt: ParsedKey): Keybind.Info {
if (evt.name === "\x1F")
return {
ctrl: true,
name: "_",
shift: false,
leader: false,
meta: false,
}
return {
ctrl: evt.ctrl,
name: evt.name,
shift: evt.shift,
leader: store.leader,
meta: evt.meta,
// Handle special case for Ctrl+Underscore (represented as \x1F)
if (evt.name === "\x1F") {
return Keybind.fromParsedKey({ ...evt, name: "_", ctrl: true }, store.leader)
}
return Keybind.fromParsedKey(evt, store.leader)
},
match(key: keyof KeybindsConfig, evt: ParsedKey) {
const keybind = keybinds()[key]
@@ -5,8 +5,13 @@ import type { TextPart } from "@opencode-ai/sdk/v2"
import { Locale } from "@/util/locale"
import { DialogMessage } from "./dialog-message"
import { useDialog } from "../../ui/dialog"
import type { PromptInfo } from "../../component/prompt/history"
export function DialogTimeline(props: { sessionID: string; onMove: (messageID: string) => void }) {
export function DialogTimeline(props: {
sessionID: string
onMove: (messageID: string) => void
setPrompt?: (prompt: PromptInfo) => void
}) {
const sync = useSync()
const dialog = useDialog()
@@ -26,10 +31,13 @@ export function DialogTimeline(props: { sessionID: string; onMove: (messageID: s
value: message.id,
footer: Locale.time(message.time.created),
onSelect: (dialog) => {
dialog.replace(() => <DialogMessage messageID={message.id} sessionID={props.sessionID} />)
dialog.replace(() => (
<DialogMessage messageID={message.id} sessionID={props.sessionID} setPrompt={props.setPrompt} />
))
},
})
}
result.reverse()
return result
})
@@ -289,6 +289,7 @@ export function Session() {
if (child) scroll.scrollBy(child.y - scroll.y - 1)
}}
sessionID={route.sessionID}
setPrompt={(promptInfo) => prompt.set(promptInfo)}
/>
))
},
@@ -259,9 +259,11 @@ export function Sidebar(props: { sessionID: string }) {
flexDirection="row"
gap={1}
>
<text flexShrink={0}></text>
<text flexShrink={0} fg={theme.text}>
</text>
<box flexGrow={1} gap={1}>
<text>
<text fg={theme.text}>
<b>Getting started</b>
</text>
<text fg={theme.textMuted}>OpenCode includes free models so you can start immediately.</text>
@@ -269,13 +271,16 @@ export function Sidebar(props: { sessionID: string }) {
Connect from 75+ providers to use other models, including Claude, GPT, Gemini etc
</text>
<box flexDirection="row" gap={1} justifyContent="space-between">
<text>Connect provider</text>
<text fg={theme.text}>Connect provider</text>
<text fg={theme.textMuted}>/connect</text>
</box>
</box>
</box>
</Show>
<text fg={theme.text}>{directory()}</text>
<text>
<span style={{ fg: theme.textMuted }}>{directory().split("/").slice(0, -1).join("/")}/</span>
<span style={{ fg: theme.text }}>{directory().split("/").at(-1)}</span>
</text>
<text fg={theme.textMuted}>
<span style={{ fg: theme.success }}></span> <b>Open</b>
<span style={{ fg: theme.text }}>
@@ -22,7 +22,9 @@ export function DialogAlert(props: DialogAlertProps) {
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD}>{props.title}</text>
<text attributes={TextAttributes.BOLD} fg={theme.text}>
{props.title}
</text>
<text fg={theme.textMuted}>esc</text>
</box>
<box paddingBottom={1}>
@@ -34,7 +34,9 @@ export function DialogConfirm(props: DialogConfirmProps) {
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD}>{props.title}</text>
<text attributes={TextAttributes.BOLD} fg={theme.text}>
{props.title}
</text>
<text fg={theme.textMuted}>esc</text>
</box>
<box paddingBottom={1}>
@@ -18,7 +18,9 @@ export function DialogHelp() {
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD}>Help</text>
<text attributes={TextAttributes.BOLD} fg={theme.text}>
Help
</text>
<text fg={theme.textMuted}>esc/enter</text>
</box>
<box paddingBottom={1}>
@@ -35,7 +35,9 @@ export function DialogPrompt(props: DialogPromptProps) {
return (
<box paddingLeft={2} paddingRight={2} gap={1}>
<box flexDirection="row" justifyContent="space-between">
<text attributes={TextAttributes.BOLD}>{props.title}</text>
<text attributes={TextAttributes.BOLD} fg={theme.text}>
{props.title}
</text>
<text fg={theme.textMuted}>esc</text>
</box>
<box gap={1}>
@@ -36,6 +36,7 @@ export interface DialogSelectOption<T = any> {
category?: string
disabled?: boolean
bg?: RGBA
gutter?: JSX.Element
onSelect?: (ctx: DialogContext, trigger?: "prompt") => void
}
@@ -239,7 +240,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
moveTo(index)
}}
backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)}
paddingLeft={current() ? 1 : 3}
paddingLeft={current() || option.gutter ? 1 : 3}
paddingRight={3}
gap={1}
>
@@ -249,6 +250,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
description={option.description !== category ? option.description : undefined}
active={active()}
current={current()}
gutter={option.gutter}
/>
</box>
)
@@ -282,6 +284,7 @@ function Option(props: {
active?: boolean
current?: boolean
footer?: JSX.Element | string
gutter?: JSX.Element
onMouseOver?: () => void
}) {
const { theme } = useTheme()
@@ -294,6 +297,11 @@ function Option(props: {
</text>
</Show>
<Show when={!props.current && props.gutter}>
<box flexShrink={0} marginRight={0.5}>
{props.gutter}
</box>
</Show>
<text
flexGrow={1}
fg={props.active ? fg : props.current ? theme.primary : theme.text}
@@ -61,7 +61,7 @@ export namespace Clipboard {
const getCopyMethod = lazy(() => {
const os = platform()
if (os === "darwin" && Bun.which("oascript")) {
if (os === "darwin" && Bun.which("osascript")) {
console.log("clipboard: using osascript")
return async (text: string) => {
const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
+80 -2
View File
@@ -1,5 +1,6 @@
import { Log } from "../util/log"
import path from "path"
import { pathToFileURL } from "url"
import os from "os"
import z from "zod"
import { Filesystem } from "../util/filesystem"
@@ -297,7 +298,7 @@ export namespace Config {
dot: true,
cwd: dir,
})) {
plugins.push("file://" + item)
plugins.push(pathToFileURL(item).href)
}
return plugins
}
@@ -466,7 +467,84 @@ export namespace Config {
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
input_submit: z.string().optional().default("return").describe("Submit input"),
input_newline: z.string().optional().default("shift+return,ctrl+j").describe("Insert newline in input"),
input_newline: z
.string()
.optional()
.default("shift+return,ctrl+return,alt+return,ctrl+j")
.describe("Insert newline in input"),
input_move_left: z.string().optional().default("left,ctrl+b").describe("Move cursor left in input"),
input_move_right: z.string().optional().default("right,ctrl+f").describe("Move cursor right in input"),
input_move_up: z.string().optional().default("up").describe("Move cursor up in input"),
input_move_down: z.string().optional().default("down").describe("Move cursor down in input"),
input_select_left: z.string().optional().default("shift+left").describe("Select left in input"),
input_select_right: z.string().optional().default("shift+right").describe("Select right in input"),
input_select_up: z.string().optional().default("shift+up").describe("Select up in input"),
input_select_down: z.string().optional().default("shift+down").describe("Select down in input"),
input_line_home: z.string().optional().default("ctrl+a").describe("Move to start of line in input"),
input_line_end: z.string().optional().default("ctrl+e").describe("Move to end of line in input"),
input_select_line_home: z
.string()
.optional()
.default("ctrl+shift+a")
.describe("Select to start of line in input"),
input_select_line_end: z.string().optional().default("ctrl+shift+e").describe("Select to end of line in input"),
input_visual_line_home: z.string().optional().default("alt+a").describe("Move to start of visual line in input"),
input_visual_line_end: z.string().optional().default("alt+e").describe("Move to end of visual line in input"),
input_select_visual_line_home: z
.string()
.optional()
.default("alt+shift+a")
.describe("Select to start of visual line in input"),
input_select_visual_line_end: z
.string()
.optional()
.default("alt+shift+e")
.describe("Select to end of visual line in input"),
input_buffer_home: z.string().optional().default("home").describe("Move to start of buffer in input"),
input_buffer_end: z.string().optional().default("end").describe("Move to end of buffer in input"),
input_select_buffer_home: z
.string()
.optional()
.default("shift+home")
.describe("Select to start of buffer in input"),
input_select_buffer_end: z.string().optional().default("shift+end").describe("Select to end of buffer in input"),
input_delete_line: z.string().optional().default("ctrl+shift+d").describe("Delete line in input"),
input_delete_to_line_end: z.string().optional().default("ctrl+k").describe("Delete to end of line in input"),
input_delete_to_line_start: z.string().optional().default("ctrl+u").describe("Delete to start of line in input"),
input_backspace: z.string().optional().default("backspace,shift+backspace").describe("Backspace in input"),
input_delete: z.string().optional().default("ctrl+d,delete,shift+delete").describe("Delete character in input"),
input_undo: z.string().optional().default("ctrl+-,super+z").describe("Undo in input"),
input_redo: z.string().optional().default("ctrl+.,super+shift+z").describe("Redo in input"),
input_word_forward: z
.string()
.optional()
.default("alt+f,alt+right,ctrl+right")
.describe("Move word forward in input"),
input_word_backward: z
.string()
.optional()
.default("alt+b,alt+left,ctrl+left")
.describe("Move word backward in input"),
input_select_word_forward: z
.string()
.optional()
.default("alt+shift+f,alt+shift+right")
.describe("Select word forward in input"),
input_select_word_backward: z
.string()
.optional()
.default("alt+shift+b,alt+shift+left")
.describe("Select word backward in input"),
input_delete_word_forward: z
.string()
.optional()
.default("alt+d,alt+delete,ctrl+delete")
.describe("Delete word forward in input"),
input_delete_word_backward: z
.string()
.optional()
.default("ctrl+w,ctrl+backspace,alt+backspace")
.describe("Delete word backward in input"),
history_previous: z.string().optional().default("up").describe("Previous history item"),
history_next: z.string().optional().default("down").describe("Next history item"),
session_child_cycle: z.string().optional().default("<leader>right").describe("Next child session"),
+2 -1
View File
@@ -1,5 +1,6 @@
export namespace Flag {
export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE")
export const OPENCODE_GIT_BASH_PATH = process.env["OPENCODE_GIT_BASH_PATH"]
export const OPENCODE_CONFIG = process.env["OPENCODE_CONFIG"]
export const OPENCODE_CONFIG_DIR = process.env["OPENCODE_CONFIG_DIR"]
export const OPENCODE_CONFIG_CONTENT = process.env["OPENCODE_CONFIG_CONTENT"]
@@ -11,12 +12,12 @@ export namespace Flag {
export const OPENCODE_ENABLE_EXPERIMENTAL_MODELS = truthy("OPENCODE_ENABLE_EXPERIMENTAL_MODELS")
export const OPENCODE_DISABLE_AUTOCOMPACT = truthy("OPENCODE_DISABLE_AUTOCOMPACT")
export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
export const OPENCODE_CLIENT = process.env["OPENCODE_CLIENT"] ?? "cli"
// Experimental
export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL")
export const OPENCODE_EXPERIMENTAL_ICON_DISCOVERY =
OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY")
export const OPENCODE_EXPERIMENTAL_WATCHER = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_WATCHER")
export const OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT = truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT")
export const OPENCODE_ENABLE_EXA =
truthy("OPENCODE_ENABLE_EXA") || OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_EXA")
+18
View File
@@ -275,3 +275,21 @@ export const terraform: Info = {
return Bun.which("terraform") !== null
},
}
export const latexindent: Info = {
name: "latexindent",
command: ["latexindent", "-w", "-s", "$FILE"],
extensions: [".tex"],
async enabled() {
return Bun.which("latexindent") !== null
},
}
export const gleam: Info = {
name: "gleam",
command: ["gleam", "format", "$FILE"],
extensions: [".gleam"],
async enabled() {
return Bun.which("gleam") !== null
},
}
+2 -1
View File
@@ -6,6 +6,7 @@ import z from "zod"
import { NamedError } from "@opencode-ai/util/error"
import { Log } from "../util/log"
import { iife } from "@/util/iife"
import { Flag } from "../flag/flag"
declare global {
const OPENCODE_VERSION: string
@@ -162,7 +163,7 @@ export namespace Installation {
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 const USER_AGENT = `opencode/${CHANNEL}/${VERSION}/${Flag.OPENCODE_CLIENT}`
export async function latest(installMethod?: Method) {
const detectedMethod = installMethod || (await method())
+7 -6
View File
@@ -1,6 +1,7 @@
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import path from "path"
import { pathToFileURL, fileURLToPath } from "url"
import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node"
import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"
import { Log } from "../util/log"
@@ -46,7 +47,7 @@ export namespace LSPClient {
const diagnostics = new Map<string, Diagnostic[]>()
connection.onNotification("textDocument/publishDiagnostics", (params) => {
const path = new URL(params.uri).pathname
const path = fileURLToPath(params.uri)
l.info("textDocument/publishDiagnostics", {
path,
})
@@ -68,7 +69,7 @@ export namespace LSPClient {
connection.onRequest("workspace/workspaceFolders", async () => [
{
name: "workspace",
uri: "file://" + input.root,
uri: pathToFileURL(input.root).href,
},
])
connection.listen()
@@ -76,12 +77,12 @@ export namespace LSPClient {
l.info("sending initialize")
await withTimeout(
connection.sendRequest("initialize", {
rootUri: "file://" + input.root,
rootUri: pathToFileURL(input.root).href,
processId: input.server.process.pid,
workspaceFolders: [
{
name: "workspace",
uri: "file://" + input.root,
uri: pathToFileURL(input.root).href,
},
],
initializationOptions: {
@@ -154,7 +155,7 @@ export namespace LSPClient {
})
await connection.sendNotification("textDocument/didChange", {
textDocument: {
uri: `file://` + input.path,
uri: pathToFileURL(input.path).href,
version: next,
},
contentChanges: [{ text }],
@@ -166,7 +167,7 @@ export namespace LSPClient {
diagnostics.delete(input.path)
await connection.sendNotification("textDocument/didOpen", {
textDocument: {
uri: `file://` + input.path,
uri: pathToFileURL(input.path).href,
languageId,
version: 0,
text,
+2 -1
View File
@@ -3,6 +3,7 @@ import { Bus } from "@/bus"
import { Log } from "../util/log"
import { LSPClient } from "./client"
import path from "path"
import { pathToFileURL } from "url"
import { LSPServer } from "./server"
import z from "zod"
import { Config } from "../config/config"
@@ -270,7 +271,7 @@ export namespace LSP {
return run((client) => {
return client.connection.sendRequest("textDocument/hover", {
textDocument: {
uri: `file://${input.file}`,
uri: pathToFileURL(input.file).href,
},
position: {
line: input.line,
+1
View File
@@ -34,6 +34,7 @@ export const LANGUAGE_EXTENSIONS: Record<string, string> = {
".gitrebase": "git-rebase",
".go": "go",
".groovy": "groovy",
".gleam": "gleam",
".hbs": "handlebars",
".handlebars": "handlebars",
".hs": "haskell",
+141
View File
@@ -1386,4 +1386,145 @@ export namespace LSPServer {
}
},
}
export const TexLab: Info = {
id: "texlab",
extensions: [".tex", ".bib"],
root: NearestRoot([".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"]),
async spawn(root) {
let bin = Bun.which("texlab", {
PATH: process.env["PATH"] + ":" + Global.Path.bin,
})
if (!bin) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("downloading texlab from GitHub releases")
const response = await fetch("https://api.github.com/repos/latex-lsp/texlab/releases/latest")
if (!response.ok) {
log.error("Failed to fetch texlab release info")
return
}
const release = (await response.json()) as {
tag_name?: string
assets?: { name?: string; browser_download_url?: string }[]
}
const version = release.tag_name?.replace("v", "")
if (!version) {
log.error("texlab release did not include a version tag")
return
}
const platform = process.platform
const arch = process.arch
const texArch = arch === "arm64" ? "aarch64" : "x86_64"
const texPlatform = platform === "darwin" ? "macos" : platform === "win32" ? "windows" : "linux"
const ext = platform === "win32" ? "zip" : "tar.gz"
const assetName = `texlab-${texArch}-${texPlatform}.${ext}`
const assets = release.assets ?? []
const asset = assets.find((a) => a.name === assetName)
if (!asset?.browser_download_url) {
log.error(`Could not find asset ${assetName} in texlab release`)
return
}
const downloadResponse = await fetch(asset.browser_download_url)
if (!downloadResponse.ok) {
log.error("Failed to download texlab")
return
}
const tempPath = path.join(Global.Path.bin, assetName)
await Bun.file(tempPath).write(downloadResponse)
if (ext === "zip") {
await $`unzip -o -q ${tempPath}`.cwd(Global.Path.bin).nothrow()
}
if (ext === "tar.gz") {
await $`tar -xzf ${tempPath}`.cwd(Global.Path.bin).nothrow()
}
await fs.rm(tempPath, { force: true })
bin = path.join(Global.Path.bin, "texlab" + (platform === "win32" ? ".exe" : ""))
if (!(await Bun.file(bin).exists())) {
log.error("Failed to extract texlab binary")
return
}
if (platform !== "win32") {
await $`chmod +x ${bin}`.nothrow()
}
log.info("installed texlab", { bin })
}
return {
process: spawn(bin, {
cwd: root,
}),
}
},
}
export const DockerfileLS: Info = {
id: "dockerfile",
extensions: [".dockerfile", "Dockerfile"],
root: async () => Instance.directory,
async spawn(root) {
let binary = Bun.which("docker-langserver")
const args: string[] = []
if (!binary) {
const js = path.join(Global.Path.bin, "node_modules", "dockerfile-language-server-nodejs", "lib", "server.js")
if (!(await Bun.file(js).exists())) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
await Bun.spawn([BunProc.which(), "install", "dockerfile-language-server-nodejs"], {
cwd: Global.Path.bin,
env: {
...process.env,
BUN_BE_BUN: "1",
},
stdout: "pipe",
stderr: "pipe",
stdin: "pipe",
}).exited
}
binary = BunProc.which()
args.push("run", js)
}
args.push("--stdio")
const proc = spawn(binary, args, {
cwd: root,
env: {
...process.env,
BUN_BE_BUN: "1",
},
})
return {
process: proc,
}
},
}
export const Gleam: Info = {
id: "gleam",
extensions: [".gleam"],
root: NearestRoot(["gleam.toml"]),
async spawn(root) {
const gleam = Bun.which("gleam")
if (!gleam) {
log.info("gleam not found, please install gleam first")
return
}
return {
process: spawn(gleam, ["lsp"], {
cwd: root,
}),
}
},
}
}
+26 -8
View File
@@ -199,14 +199,29 @@ export namespace ProviderTransform {
}
export function temperature(model: Provider.Model) {
if (model.api.id.toLowerCase().includes("qwen")) return 0.55
if (model.api.id.toLowerCase().includes("claude")) return undefined
if (model.api.id.toLowerCase().includes("gemini-3-pro")) return 1.0
return 0
const id = model.id.toLowerCase()
if (id.includes("qwen")) return 0.55
if (id.includes("claude")) return undefined
if (id.includes("gemini-3-pro")) return 1.0
if (id.includes("glm-4.6")) return 1.0
if (id.includes("minimax-m2")) return 1.0
// if (id.includes("kimi-k2")) {
// if (id.includes("thinking")) return 1.0
// return 0.6
// }
return undefined
}
export function topP(model: Provider.Model) {
if (model.api.id.toLowerCase().includes("qwen")) return 1
const id = model.id.toLowerCase()
if (id.includes("qwen")) return 1
if (id.includes("minimax-m2")) return 0.95
return undefined
}
export function topK(model: Provider.Model) {
const id = model.id.toLowerCase()
if (id.includes("minimax-m2")) return 40
return undefined
}
@@ -226,7 +241,10 @@ export namespace ProviderTransform {
}
}
if (model.providerID === "baseten") {
if (
model.providerID === "baseten" ||
(model.providerID === "opencode" && ["kimi-k2-thinking", "glm-4.6"].includes(model.api.id))
) {
result["chat_template_args"] = { enable_thinking: true }
}
@@ -252,7 +270,7 @@ export namespace ProviderTransform {
result["reasoningEffort"] = "medium"
}
if (model.api.id.endsWith("gpt-5.1") && model.providerID !== "azure") {
if (model.api.id.endsWith("gpt-5.") && model.providerID !== "azure") {
result["textVerbosity"] = "low"
}
@@ -269,7 +287,7 @@ export namespace ProviderTransform {
const options: Record<string, any> = {}
if (model.providerID === "openai" || model.api.id.includes("gpt-5")) {
if (model.api.id.includes("5.1")) {
if (model.api.id.includes("5.")) {
options["reasoningEffort"] = "low"
} else {
options["reasoningEffort"] = "minimal"
+2 -2
View File
@@ -6,10 +6,10 @@ import { Identifier } from "../id/id"
import { Log } from "../util/log"
import type { WSContext } from "hono/ws"
import { Instance } from "../project/instance"
import { shell } from "@opencode-ai/util/shell"
import { lazy } from "@opencode-ai/util/lazy"
import {} from "process"
import { Installation } from "@/installation"
import { Shell } from "@/shell/shell"
export namespace Pty {
const log = Log.create({ service: "pty" })
@@ -112,7 +112,7 @@ export namespace Pty {
export async function create(input: CreateInput) {
const id = Identifier.create("pty", false)
const command = input.command || shell()
const command = input.command || Shell.preferred()
const args = input.args || []
const cwd = input.cwd || Instance.directory
const env = { ...process.env, ...input.env } as Record<string, string>

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