Compare commits

..
Author SHA1 Message Date
Dax Raad 0023dd0813 ci: remove heavy nix-desktop workflow 2026-01-29 14:10:11 -05:00
Dax Raad 8787b527d4 ci: remove push triggers from workflow files 2026-01-29 14:06:15 -05:00
DaxandGitHub cf5cf7b23e chore: consolidate and standardize workflow files (#11183) 2026-01-29 19:04:12 +00:00
DaxandGitHub a9a7595234 test: skip failing tests (#11184) 2026-01-29 18:57:59 +00:00
9ed3b0742f ci (#11149)
Co-authored-by: opencode <opencode@sst.dev>
2026-01-29 13:17:55 -05:00
GitHub Action ae9199e101 chore: generate 2026-01-29 18:14:15 +00:00
Aiden Cline f996e05b42 chore: format code 2026-01-29 12:13:29 -06:00
Aiden Cline 8dedb3f4ae chore: regen sdk 2026-01-29 11:49:22 -06:00
45ec3105b1 feat: support config skill registration (#9640)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-01-29 11:47:06 -06:00
Frank 5a56e8172f zen: m2.1 and glm4.7 free models 2026-01-29 12:29:23 -05:00
Aiden Cline aa92ef37fd tweak: add 'skill' to permissions config section so that ides will show it as autocomplete option (this is already a respected setting) 2026-01-29 10:55:39 -06:00
d5c59a66c1 ci: added gh workflow that adds 'contributor' label to PRs/Issues (#11118)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-01-29 10:38:08 -06:00
Aiden Cline 301895c7f7 fix: ensure kimi k2.5 from fireworks ai and kimi for coding providers properly set temperature 2026-01-29 10:30:22 -06:00
Ravi KumarandGitHub 03ba49af4e fix(telemetry): restore userId and sessionId metadata in experimental_telemetry (#8195) 2026-01-29 10:14:28 -06:00
GitHub Action 008ad54cb5 ignore: update download stats 2026-01-29 2026-01-29 12:07:50 +00:00
Ryan VogelandGitHub 571751c313 fix: remove redundant Highlights heading from publish template (#11121) 2026-01-29 06:16:45 -05:00
Github Action 82717f6e8b chore: update nix node_modules hashes 2026-01-29 07:12:38 +00:00
GitHub Action c946f5f7eb chore: generate 2026-01-29 07:10:34 +00:00
2af326606c feat(desktop): Add desktop deep link (#10072)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-01-29 15:09:53 +08:00
opencode 7c0067d59d release: v1.1.42
Update Nix Hashes / update-node-modules-hashes (push) Has been cancelled
2026-01-29 05:59:07 +00:00
Aiden Cline 92eb982863 fix: undo change that used anthropic messages endpoint for anthropic models on copilot due to ratelimiting issues, go back to completions endpoint instead 2026-01-28 23:52:23 -06:00
33c5c100ff fix: frontmatter was adding newlines in some cases causing invalid model ids (#11095)
Co-authored-by: aptdnfapt <aptdnfapt@users.noreply.github.com>
2026-01-28 23:40:59 -06:00
Aiden Cline 0fabdccf11 fix: ensure that kimi doesnt have fake variants available 2026-01-28 23:40:44 -06:00
Sebastian Herrlinger 41ea4694db more timeout race guards 2026-01-29 00:17:39 -05:00
Ariane EmoryandGitHub e84d92da28 feat: Sequential numbering for forked session titles (Issue #10105) (#10321) 2026-01-28 23:08:35 -06:00
Sebastian Herrlinger 58ba486375 guard destroyed input field in timeout 2026-01-28 23:53:37 -05:00
Aiden Cline 121016af81 ci: adjust team 2026-01-28 22:28:48 -06:00
Benjamin OldenburgandGitHub f40bdd1ac3 feat(cli): include cache tokens in stats (#10582) 2026-01-28 22:03:44 -06:00
GitHub Action efc9303b27 chore: generate 2026-01-29 04:03:01 +00:00
Aiden Cline b938e1ea99 test: fix test 2026-01-28 22:02:14 -06:00
opencode 9cfdbbb1af release: v1.1.41
Update Nix Hashes / update-node-modules-hashes (push) Has been cancelled
2026-01-29 04:00:39 +00:00
Aiden Cline 29ea9fcf25 fix: ensure variants for copilot models work w/ maxTokens being set 2026-01-28 21:55:50 -06:00
idealloveandGitHub 870c38a6aa fix: maxOutputTokens was accidentally hardcoded to undefined (#10995) 2026-01-28 21:28:15 -06:00
b937fe9450 fix(provider): include providerID in SDK cache key (#11020)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 21:26:26 -06:00
tanandGitHub 427ef95f7d fix(opencode): allow media-src data: URL for small audio files (#11082) 2026-01-28 21:21:49 -06:00
GitHub Action d8fe12aafc chore: generate 2026-01-29 03:21:26 +00:00
Devin GriffinandGitHub a7d7f5bb07 fix(app): make settings more responsive for mobile and small web/desktop (#10775) 2026-01-28 21:20:34 -06:00
BabouandGitHub 8cdb82038a docs: update experimental environment variables in CLI docs (#11030) 2026-01-28 20:20:13 -06:00
Brendan Allan a9e757b5bb fix(app): types 2026-01-29 10:14:42 +08:00
Aiden Cline 4d2696e027 tweak: add ctx.abort to grep tool 2026-01-28 20:06:36 -06:00
Alex YaroshukandGitHub 0c8de47f7d fix(app): file tabs - auto scroll on open & scroll via mouse wheel (#11070) 2026-01-28 19:45:12 -06:00
Julian CoyandGitHub 7ad165fbdc fix(typescript errors): remove duplicate keys causing typescript failures (#11071) 2026-01-28 19:41:50 -06:00
Goni ZahavyandGitHub e5b33f8a5e fix(opencode): add AbortSignal support to Ripgrep.files() and GlobTool (#10833) 2026-01-28 18:47:09 -06:00
Max KongandGitHub 90a7e3d64e fix(ui): improve zh duration display formatting (#10844) 2026-01-28 16:53:37 -06:00
Max KongandGitHub 33dc70b754 fix(opencode): normalize zh punctuation for Chinese UI (#10842) 2026-01-28 16:52:04 -06:00
GitHub Action 832bbba616 chore: generate 2026-01-28 22:51:20 +00:00
Max KongandGitHub 40d5d14304 fix(app): fill missing zh keys to avoid English fallback (#10841) 2026-01-28 16:50:50 -06:00
Alex YaroshukandGitHub 36df0d823a fix(app): alignment and padding in dialogs (#10866) 2026-01-28 16:50:00 -06:00
zerone0xandGitHub 9424f829eb fix(ui): allow KaTeX inline math to be followed by punctuation (#11033) 2026-01-28 16:44:35 -06:00
GitHub Action 7b561be159 chore: generate 2026-01-28 22:35:01 +00:00
Ryan VogelandGitHub c60464de07 fix(script): remove highlights template from release notes (#11052) 2026-01-28 17:34:14 -05:00
opencode 4e41ca74b9 release: v1.1.40
Update Nix Hashes / update-node-modules-hashes (push) Has been cancelled
2026-01-28 20:09:37 +00:00
Tommy D. RossiandGitHub 8c05eb22b1 fix(markdown): Add streaming prop to markdown element (#11025) 2026-01-28 12:55:57 -05:00
adamelmore f607353be6 fix(app): close review pane 2026-01-28 10:27:01 -06:00
GitHub Action af3c97f192 chore: generate 2026-01-28 16:12:32 +00:00
26e14ce628 fix: add SubtaskPart with metadata reference (#10990)
Co-authored-by: yangtianzhe <yangtianzhe@corp.netease.com>
2026-01-28 11:11:43 -05:00
SairajandGitHub 57ad1814e3 fix(desktop): enable ctrl+n and ctrl+p for popover navigation (#10777) 2026-01-28 08:12:27 -06:00
GitHub Action 4f60ea6108 chore: generate 2026-01-28 14:11:30 +00:00
Nattawee PhantawongandGitHub 775d288027 feat(i18n): add th locale support (#10809) 2026-01-28 08:10:50 -06:00
GitHub Action bc4968abbb chore: generate 2026-01-28 13:28:52 +00:00
adamelmore acb92fcd34 chore: cleanup 2026-01-28 07:28:03 -06:00
adamelmore c9bbea4266 chore: cleanup 2026-01-28 07:28:03 -06:00
adamelmore 65e1186efe wip(app): global config 2026-01-28 07:28:03 -06:00
adamelmore 8faa2ffcf8 chore: cleanup 2026-01-28 07:28:02 -06:00
adamelmore bdfd8f8b0f feat(app): custom provider 2026-01-28 07:28:02 -06:00
adamelmore 2f35c40bb5 chore(app): global config changes 2026-01-28 07:28:02 -06:00
GitHub Action 6650aa6f35 ignore: update download stats 2026-01-28 2026-01-28 12:05:45 +00:00
Aiden Clineandopencode 8798a77a72 bump: plugins 2026-01-28 07:19:18 +00:00
opencode 6eb2bdd665 release: v1.1.39
Update Nix Hashes / update-node-modules-hashes (push) Has been cancelled
2026-01-28 07:19:17 +00:00
GitHub Action f97197bdb0 chore: generate 2026-01-28 07:05:16 +00:00
Aiden Cline 5a0b3ee673 fix: ensure copilot plugin properly sets headers for new messages api 2026-01-28 02:04:30 -05:00
opencode 3bb10773e6 release: v1.1.38
Update Nix Hashes / update-node-modules-hashes (push) Has been cancelled
2026-01-28 06:42:23 +00:00
Aiden Cline 558590712d fix: ensure parallel tool calls dont double load AGENTS.md 2026-01-28 01:38:10 -05:00
Dax Raad d76e1448f0 ci 2026-01-28 01:33:34 -05:00
Aiden Cline 026b3cc88d bump plugin version 2026-01-28 01:31:01 -05:00
Aiden Cline 28e5557bf4 ignore: adjust flag 2026-01-28 01:09:31 -05:00
GitHub Action 9a8da20a97 chore: generate 2026-01-28 06:05:21 +00:00
Goni ZahavyandGitHub 63f5669eb5 fix(opencode): ensure unsub(PartUpdated) is always called in TaskTool (#9992) 2026-01-28 01:04:43 -05:00
Frank 427cc3e153 zen: kimi k2.5 2026-01-28 00:09:33 -05:00
TitoandGitHub aedd760141 fix(cli): restore brand integrity of CLI wordmark (#10912) 2026-01-27 22:24:02 -05:00
GitHub Action 6da9fb8fb9 chore: generate 2026-01-28 03:10:48 +00:00
James MurdzaandGitHub b73e0240a5 docs: add Daytona OpenCode plugin to ecosystem (#10917) 2026-01-27 22:09:55 -05:00
GitHub Action 5f2a7c630b chore: generate 2026-01-28 03:06:58 +00:00
Tommy D. RossiandGitHub 7988f52231 feat(app): use opentui markdown component behind experimental flag (#10900) 2026-01-27 22:06:09 -05:00
opencode e3be4c9f23 release: v1.1.37
Update Nix Hashes / update-node-modules-hashes (push) Has been cancelled
2026-01-28 02:35:38 +00:00
adamelmore d9741866c5 fix(app): reintroduce review tab 2026-01-27 20:24:27 -06:00
898118bafb feat: support headless authentication for chatgpt/codex (#10890)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-01-27 19:05:52 -05:00
adamelmore b4a9e1b190 fix(app): auto-scroll 2026-01-27 17:48:58 -06:00
Alex YaroshukandGitHub 15ffd3cba1 feat(app): add 'connect provider' button to the manage models dialog (#10887) 2026-01-27 17:26:15 -06:00
adamelmore df7f9ae3f4 fix(app): terminal corruption 2026-01-27 16:51:57 -06:00
adamelmore d17ba84ee1 fix(app): file tree not always loading 2026-01-27 16:13:11 -06:00
adamelmore 5c8580a187 test(app): fix outdated e2e test 2026-01-27 16:11:58 -06:00
adamelmore 13b2587e96 test(app): fix outdated e2e test 2026-01-27 15:59:01 -06:00
adamelmore 605e533558 fix(app): file tree not always loading 2026-01-27 15:59:01 -06:00
adamelmore 33d400c567 fix(app): spinner color 2026-01-27 15:59:01 -06:00
Aiden Cline b8e726521d fix(tui): handle 4-5 codes too in c to copy logic 2026-01-27 16:29:01 -05:00
Github Action 95632d893b chore: update nix node_modules hashes 2026-01-27 21:28:23 +00:00
adamelmore 1d5ee3e587 fix(app): not auto-navigating to last project 2026-01-27 15:25:07 -06:00
adamelmore e5b18674f9 feat(desktop): tauri locales 2026-01-27 15:25:07 -06:00
adamelmore 51edf68606 feat(desktop): i18n for tauri side 2026-01-27 15:25:07 -06:00
adamelmore acf0df1e98 chore: cleanup 2026-01-27 15:25:07 -06:00
adamelmore 842f17d6d9 perf(app): better memory management 2026-01-27 15:25:07 -06:00
adamelmore 1ebf63c70c fix(app): don't connect to localhost through vpn 2026-01-27 15:25:06 -06:00
adamelmore d7948c2376 fix(app): auto-scroll 2026-01-27 15:25:06 -06:00
adamelmore 892113ab39 chore(app): show 5 highlights 2026-01-27 15:25:06 -06:00
David Hill f2bf620206 fix(app): highlight selected change
Track clicked file in the Changes tree and apply selection styling to the matching review diff.
2026-01-27 20:22:05 +00:00
David Hill 00c7729658 fix(app): set filetree padding to 6px 2026-01-27 20:13:52 +00:00
David Hill 18d6c2191c fix(app): align filetree change styling 2026-01-27 20:13:52 +00:00
David Hill d15201d11d fix(app): delay nav tooltips 2026-01-27 20:13:52 +00:00
David Hill 1fffbc6fb3 fix(app): adjust titlebar left spacing 2026-01-27 20:13:52 +00:00
David Hill 2ca69ac953 fix(app): shorten nav tooltips 2026-01-27 20:13:52 +00:00
David Hill 8ee5376f9b feat(app): add filetree tooltips with diff labels 2026-01-27 20:13:52 +00:00
David Hill 82068955f7 feat(app): color filetree change dots by diff kind 2026-01-27 20:13:52 +00:00
Frank df8b23db9e Revert "Set temperature for kimi k2.5"
This reverts commit bb63d16fa8.
2026-01-27 14:48:08 -05:00
Frank 2649dcae7f Revert "ci: make tests passing a requirement pre-release"
This reverts commit 8c00818108.
2026-01-27 14:37:33 -05:00
Frank bb63d16fa8 Set temperature for kimi k2.5 2026-01-27 14:13:21 -05:00
Aiden Cline 32ce0f4b0d tweak: add recommended topP/temp for kimi k2.5 2026-01-27 12:43:30 -05:00
GitHub Action 6284565de2 chore: generate 2026-01-27 17:42:26 +00:00
adamelmore 7de42ca242 feat(app): improved layout 2026-01-27 11:40:39 -06:00
adamelmore e2c57735b4 fix(app): session diffs not always loading 2026-01-27 11:38:37 -06:00
adamelmore 07d84fe008 feat(app): show loaded agents.md files 2026-01-27 11:38:37 -06:00
adamelmore b9edd23608 test(app): new e2e smoke tests 2026-01-27 11:38:36 -06:00
adamelmore 06e3c4a4f2 chore(app): translations 2026-01-27 11:38:36 -06:00
adamelmore 2f5a238b51 feat(app): update settings in general settings 2026-01-27 11:38:35 -06:00
Frank 173faca3c6 zen: kimi k2.5 and minimax m2.1 2026-01-27 12:07:00 -05:00
Aiden Cline 8c00818108 ci: make tests passing a requirement pre-release 2026-01-27 11:31:02 -05:00
Aiden Cline f12f7e7718 tweak: adjust retry check to be more defensive 2026-01-27 11:31:02 -05:00
OpeOginniandGitHub 0aa93379bd chore(docs): Better explanation on how to allow tools in external directories (#10862) 2026-01-27 11:00:10 -05:00
GitHub Action dbc8d7edca chore: generate 2026-01-27 15:59:54 +00:00
d8e7e915e2 feat(opencode): Handle Venice cache creation tokens (#10735)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-01-27 10:59:12 -05:00
adamelmore 712d2b7d15 fix(app): swallow file search errors 2026-01-27 08:43:38 -06:00
adamelmore 00e79210e5 fix(app): tooltips causing getComputedStyle errors in model select 2026-01-27 08:43:38 -06:00
adamelmore 099ab929db chore(app): cleanup tailwind vs pure css 2026-01-27 08:43:37 -06:00
adamelmore eac2d4c699 fix(app): navigate to tabs when opening file 2026-01-27 08:43:37 -06:00
adamelmore 3297e5230e fix(app): open markdown links in external browser 2026-01-27 08:43:36 -06:00
adamelmore c3d8d2b27f Revert "fix(app): select model anchor"
This reverts commit 36babf5fc390fd9a892ad3742690740296d94a23.
2026-01-27 08:43:36 -06:00
adamelmore 19c7874493 fix(app): select model anchor 2026-01-27 08:43:36 -06:00
adamelmore 27bb82761b perf(app): shared terminal ghostty-web instance 2026-01-27 08:43:36 -06:00
adamelmore c7e2f1965d perf(app): cleanup connect provider timers 2026-01-27 08:43:35 -06:00
adamelmore 3e420bf8e1 perf(app): don't keep parts in memory 2026-01-27 08:43:35 -06:00
adamelmore ad624f65ee fix(app): don't show session skeleton after workspace reset 2026-01-27 08:43:34 -06:00
David Hill c68261fc06 fix(ui): add max-width 280px to tabs with text truncation 2026-01-27 14:04:10 +00:00
David Hill 2d0049f0d9 fix(app): use smaller close icon on tabs to match comment cards 2026-01-27 14:04:10 +00:00
David Hill 52387e7f33 fix(app): only show left border on plus button when sticky 2026-01-27 14:04:10 +00:00
David Hill ebfa2b57da fix(app): update review empty states to 14px and align select file empty state 2026-01-27 14:04:10 +00:00
David Hill 03b9317f49 fix(app): center filetree empty state with 32px top margin 2026-01-27 14:04:10 +00:00
David Hill 3862b1aeda fix(ui): set filetree tablist height to 48px with centered content 2026-01-27 14:04:10 +00:00
adamelmore b4e0cdbe8e docs(core): plugin tool context updates 2026-01-27 06:29:20 -06:00
adamelmore 095328faf4 fix(app): non-fatal error handling 2026-01-27 06:29:20 -06:00
adamelmore 743e83d9bf fix(app): agent fallback colors 2026-01-27 06:29:19 -06:00
adamelmore 1f9313847f feat(core): add worktree to plugin tool context 2026-01-27 06:29:19 -06:00
adamelmore 2180be2f3f chore: cleanup 2026-01-27 06:29:19 -06:00
adamelmore 58b9b54600 feat(app): forward and back buttons 2026-01-27 06:29:18 -06:00
adamelmore c0a5f85349 chore(app): missing tooltips 2026-01-27 06:29:18 -06:00
adamelmore b6565c606e fix(app): auto-scroll button sometimes sticks 2026-01-27 06:29:18 -06:00
GitHub Action ddffb34b99 ignore: update download stats 2026-01-27 2026-01-27 12:05:48 +00:00
Brendan Allan dd1624e30e desktop: deduplicate tauri configs 2026-01-27 17:48:43 +08:00
adamelmore 213c0e18ab fix(app): only show files in select dialog when clicking + tab 2026-01-26 20:07:50 -06:00
adamelmore a8c18dba82 fix(core): expose Instance.directory to custom tools 2026-01-26 20:07:47 -06:00
Aiden Cline 6cf2c3e3db fix: use Instance.directory instead of process.cwd() in read tool 2026-01-26 20:57:50 -05:00
Ryan VogelandGitHub b59aec6f04 feat: add /learn command to extract session learnings to scoped AGENTS.md files (#10717) 2026-01-26 20:50:44 -05:00
Chris YangandGitHub d9e8b2b65d fix(desktop): disable magnification gestures on macOS (#10605) 2026-01-26 18:50:51 -06:00
Rahul A MistryandGitHub 7655f51e10 fix(app): add connect provier in model selector (#10706) 2026-01-26 18:46:04 -06:00
GitHub Action e7c6267323 chore: generate 2026-01-27 00:45:43 +00:00
Fabio MartinoandGitHub 64a3661a32 docs: add Italian README (#10732) 2026-01-26 18:45:05 -06:00
Ryan VogelandGitHub bf463aee04 feat(release): add highlights template to draft releases (#10745) 2026-01-26 19:37:54 -05:00
adamelmore b24fd90fe8 test(app): file tree spec 2026-01-26 17:42:23 -06:00
adamelmore 6897bb7d02 chore: cleanup 2026-01-26 17:02:56 -06:00
adamelmore 8371ba5aec chore: cleanup 2026-01-26 17:01:04 -06:00
adamelmore 36577479c5 fix(app): enable file watcher 2026-01-26 16:57:44 -06:00
adamelmore bb178e9352 chore: cleanup 2026-01-26 16:57:43 -06:00
GitHub Action 4075f9e1ab chore: generate 2026-01-26 22:49:23 +00:00
adamelmore 021d9d105e fix(app): reactive file tree 2026-01-26 16:46:09 -06:00
adamelmore b07d7cdb71 fix(app): file tree performance 2026-01-26 16:41:18 -06:00
adamelmore 77f11dfabe chore: don't flip github draft release automatically 2026-01-26 16:30:19 -06:00
Aiden Cline 45b09c1465 tweak: when using messages api for copilot, attach anthropic beta headers 2026-01-26 17:17:54 -05:00
adamelmore ccc7aa49c3 wip: highlights 2026-01-26 15:36:59 -06:00
adamelmore 53ac394c68 wip: highlights 2026-01-26 15:36:59 -06:00
adamelmore 8b6484ac1a wip: highlights 2026-01-26 15:36:56 -06:00
adamelmore c1e840b9b2 chore: cleanup 2026-01-26 15:35:09 -06:00
David Hillandadamelmore a77df3c174 wip: new release modal
- highlight key updates or new features
- needs some transition love
- all copy including text and video placeholder
2026-01-26 15:35:09 -06:00
David Hillandadamelmore 9d1cf98192 fix: search clear icon 2026-01-26 15:35:09 -06:00
adamelmore 7b3d5f1d68 chore: cleanup 2026-01-26 15:30:01 -06:00
GitHub Action b21f82f5b0 chore: generate 2026-01-26 21:24:09 +00:00
adamelmore d82e94c209 fix(app): zen disconnect not working 2026-01-26 15:23:25 -06:00
adamelmore cbe8f265b9 fix(app): disconnect zen provider 2026-01-26 15:23:24 -06:00
Aiden Cline 8b5dde5536 tweak: retry logic to catch certain provider problems 2026-01-26 16:12:41 -05:00
Aiden Cline 6a62b44593 ci: add dry-run option to stale PR closer workflow
Allows testing stale PR closure without actually closing PRs
2026-01-26 15:42:50 -05:00
Aiden Cline c700b928e4 ci: add stale pr job 2026-01-26 15:38:40 -05:00
adamelmore ae815cca3a test(app): fix e2e test 2026-01-26 14:32:49 -06:00
David Hill 5a16d99b60 fix(app): disable tooltips in filetree tabs 2026-01-26 19:53:05 +00:00
David Hill 3f9b59c798 fix(app): move file tree toggle to right 2026-01-26 19:50:05 +00:00
David Hill c6febd8ddd fix(app): fade filetree guide lines on hover 2026-01-26 19:47:58 +00:00
David Hill 010ed55590 fix(app): dim non-deep filetree guide lines 2026-01-26 19:47:58 +00:00
David Hill 2be4598011 fix(app): reduce filetree folder indent 2026-01-26 19:47:58 +00:00
David Hill 99cd7f3468 fix(app): refine filetree row spacing and indent 2026-01-26 19:47:58 +00:00
David Hill fca0825b71 fix(app): use medium font for filetree items 2026-01-26 19:47:58 +00:00
David Hill 9babdb80cf fix(app): use chevron icons for filetree folders 2026-01-26 19:47:58 +00:00
David Hill f4392e023a fix(app): tighten filetree row spacing 2026-01-26 19:47:58 +00:00
David Hill 0e08c6c9fd fix(app): adjust filetree panel padding 2026-01-26 19:47:58 +00:00
David Hill 0dcb850a7a fix(ui): scope filetree pill tabs styling 2026-01-26 19:47:58 +00:00
David Hill d9a61cd94c feat(app): add Vercel AI Gateway provider description 2026-01-26 19:47:58 +00:00
David Hill 0f4a10f4a3 feat(app): add provider descriptions to settings 2026-01-26 19:47:58 +00:00
David Hill c551f7e47b fix(ui): reduce dialog transition in time to 150ms 2026-01-26 19:47:58 +00:00
David Hill 7962ff38b0 feat(app): add transition to command palette 2026-01-26 19:47:58 +00:00
David Hill 3ac11df66a feat(app): add transition to select provider dialog 2026-01-26 19:47:57 +00:00
David Hill 7caf59b433 fix(ui): prevent double-close and fix dialog replacement 2026-01-26 19:47:57 +00:00
David Hill 92229b44f8 feat(ui): add optional transition animations to dialog 2026-01-26 19:47:57 +00:00
David Hill 0a572afd46 fix(app): style view all button with interactive color and margin 2026-01-26 19:47:57 +00:00
David Hill ecd04a118a feat(ui): add models icon and use in settings 2026-01-26 19:47:57 +00:00
David Hill 6f3d413472 feat(ui): add providers icon and use in settings 2026-01-26 19:47:57 +00:00
David Hill 7c96d704d3 fix(app): use default cursor for env provider text 2026-01-26 19:47:57 +00:00
David Hill 9346c1ae3f fix(app): add hover text for env-connected providers 2026-01-26 19:47:57 +00:00
David Hill e0e97e9d93 fix(app): set provider row height to 56px 2026-01-26 19:47:57 +00:00
David Hill 810bc012b6 fix(ui): update button styles and disconnect button size 2026-01-26 19:47:57 +00:00
GitHub Action 36b832880d chore: generate 2026-01-26 19:37:41 +00:00
adamelmore d4e3acf17e fix(app): session sync issue 2026-01-26 13:36:36 -06:00
adamelmore 04337f6202 chore: cleanup 2026-01-26 13:03:34 -06:00
adamelmore 97aec21cb3 chore(app): missing i18n strings 2026-01-26 13:03:34 -06:00
adamelmore 319ad2a391 fix(app): session load cap 2026-01-26 12:58:51 -06:00
adamelmore de3b654dcd chore: refactor changelog 2026-01-26 12:51:35 -06:00
adamelmore 8b17ac656c test(app): e2e test for sidebar nav 2026-01-26 12:51:35 -06:00
GitHub Action 18bfc740c8 chore: generate 2026-01-26 18:50:58 +00:00
TimandGitHub 837037cd04 fix: ensure openai 404 errors are retried (#10590) 2026-01-26 13:50:04 -05:00
adamelmore b0f865eae5 chore: debug changelog 2026-01-26 12:21:27 -06:00
Aiden Cline ac53a372b0 feat: use anthropic compat messages api for anthropic models through copilot 2026-01-26 13:18:08 -05:00
Aiden Cline 7795cae0b5 ignore: tweak ai deps 2026-01-26 13:18:08 -05:00
adamelmore 4c9d879624 Revert "fix(app): restore external link opening in system browser (#10697)"
This reverts commit 984518b1c0.
2026-01-26 12:09:02 -06:00
adamelmore 3fdd08d66e chore: fix changelog page 2026-01-26 11:58:02 -06:00
ec2ab639bb fix(enterprise): add message navigation to share page desktop view (#10071)
Co-authored-by: Claude <noreply@anthropic.com>
2026-01-26 12:32:16 -05:00
adamelmore d05ed5ca83 chore(app): createStore over signals 2026-01-26 11:26:17 -06:00
adamelmore 37f1a1a4ef chore: cleanup 2026-01-26 11:07:52 -06:00
adamelmore b8e8d82323 chore: cleanup 2026-01-26 11:07:52 -06:00
adamelmore 801eb5d2cb wip(app): file tree mode 2026-01-26 11:07:52 -06:00
adamelmore ebeed03115 wip(app): file tree mode 2026-01-26 11:07:51 -06:00
Adamandadamelmore d9eed4c6ca feat(app): file tree 2026-01-26 11:07:51 -06:00
Ryan VogelandGitHub 7e34d27b77 fix: add 44px top padding to sticky version header on changelog (#10715) 2026-01-26 12:02:20 -05:00
Rahul A MistryandGitHub 783121c06e fix(ui): use focus-visible instead of focus to prevent sticky hover effect on click (#10651) 2026-01-26 10:58:29 -06:00
Alex YaroshukandGitHub 984518b1c0 fix(app): restore external link opening in system browser (#10697) 2026-01-26 10:57:49 -06:00
Sam HuckabyandGitHub 7fcdbd155b fix(app): Order themes alphabetically (#10698) 2026-01-26 10:55:43 -06:00
Ryan VogelandGitHub 5856ea4e75 fix: move changelog footer outside content div to fix padding (#10712) 2026-01-26 11:46:56 -05:00
Github Action 32a0dcedcb chore: update nix node_modules hashes 2026-01-26 16:21:24 +00:00
Sebastian Herrlinger f48784d152 upgrade opentui to v0.1.75 2026-01-26 11:17:26 -05:00
adamelmore 3dce6a6608 chore: gen changelog page off changelog json 2026-01-26 09:53:10 -06:00
Aiden ClineandGitHub 39a73d4894 feat: dynamically resolve AGENTS.md files from subdirectories as agent explores them (#10678) 2026-01-26 10:49:41 -05:00
Steffen DeuschandGitHub b1fbfa7e94 feat(opencode): add agent description (#10680) 2026-01-26 10:43:10 -05:00
GitHub Action 805ae19c9a chore: generate 2026-01-26 15:34:31 +00:00
adamelmore fcea7e18a5 chore: stale-while-revalidate headers for changelog 2026-01-26 09:33:43 -06:00
adamelmore 7c34319b19 fix(app): query selector with non-latin chars 2026-01-26 09:33:42 -06:00
adamelmore cd4676171b feat(app): better sidebar hover when collapsed 2026-01-26 09:33:42 -06:00
adamelmore 7016be0739 wip(app): full-height sidebar 2026-01-26 09:33:42 -06:00
adamelmore ff35db0360 wip(app): full-height sidebar 2026-01-26 09:33:42 -06:00
adamelmore af3d8c383e wip(app): sidebar hover full 2026-01-26 09:33:42 -06:00
GitHub Action 7f75f71f6b chore: generate 2026-01-26 14:15:46 +00:00
adamelmore 84b12a8fb7 feat(app): model settings 2026-01-26 08:15:01 -06:00
adamelmore 1934ee13d8 wip(app): model settings 2026-01-26 08:15:01 -06:00
adamelmore 6c1e18f111 fix(app): line selection waits on ready 2026-01-26 08:15:00 -06:00
adamelmore 3296b90372 fix(app): handle non-tool call permissions 2026-01-26 08:15:00 -06:00
adamelmore 0d651eab3b feat(app): default servers on web 2026-01-26 08:15:00 -06:00
Ariane EmoryandGitHub 0edd304f42 fix: Make diff wrapping toggle always available in command_list and correct a type error (resolves #10682) (#10683) 2026-01-26 09:01:23 -05:00
Noam BresslerandGitHub 6b83b172ae fix: await SessionRevert.cleanup for shell (#10669) 2026-01-26 07:36:17 -05:00
Idris GadiandGitHub 444934a4c1 fix(tui): add visual feedback for diff wrap and conceal toggles (#10655) 2026-01-26 07:34:09 -05:00
adamelmore c4f1087e58 chore: details 2026-01-26 06:24:47 -06:00
adamelmore c87232d5df perf(app): performance improvements 2026-01-26 06:08:09 -06:00
adamelmore d03c5f6b3f perf(app): performance improvements 2026-01-26 06:08:09 -06:00
GitHub Action 9a33b1ec88 ignore: update download stats 2026-01-26 2026-01-26 12:06:03 +00:00
Aiden Cline 022265829e ignore: update AGENTS.md to state that inference should be used 2026-01-26 00:04:00 -05:00
Ryan VogelandGitHub 23d85f4739 docs: add warning about Claude Pro/Max subscription support (#10595) 2026-01-25 22:28:01 -05:00
Ryan Vogel 8c4bf225f2 fix(web): update spacing on the changelog page 2026-01-25 21:41:12 -05:00
adamelmoreandAdam 99ae3a7717 chore: cleanup 2026-01-25 20:40:00 -06:00
adamelmoreandAdam 9d35a0bcb6 chore: cleanup 2026-01-25 20:40:00 -06:00
adamelmoreandAdam a09a8701ae chore: cleanup 2026-01-25 20:40:00 -06:00
adamelmoreandAdam 00d960d080 chore: cleanup 2026-01-25 20:40:00 -06:00
adamelmoreandAdam 5993a098b4 fix(core): don't override source in custom provider loaders 2026-01-25 20:40:00 -06:00
adamelmoreandAdam c323d96deb wip(app): provider settings 2026-01-25 20:40:00 -06:00
adamelmoreandAdam 03d884797c wip(app): provider settings 2026-01-25 20:40:00 -06:00
adamelmore a5b72a7d99 fix(ui): tab click hit area 2026-01-25 20:32:59 -06:00
Ryan VogelandDax Raad cc0085676b Add collapsible sections, sticky version header, and style refinements for changelog highlights 2026-01-25 21:26:56 -05:00
Aiden ClineandDax Raad eaad75b176 tweak: adjust tui syncing logic to help prevent case where agents would be undefined / missing 2026-01-25 21:26:47 -05:00
Ryan VogelandDax Raad ab3268896d Add highlight tag parsing for changelog with video support 2026-01-25 21:26:43 -05:00
adamelmore 3d23d2df71 fix(app): missing translations for status 2026-01-25 19:39:09 -06:00
Aiden Cline 578361de64 fix: remove broken app.tsx command option 2026-01-25 20:27:41 -05:00
adamelmore 5369e96ab7 fix(app): line selection colors 2026-01-25 19:07:24 -06:00
adamelmore fbcf138526 chore: better i18n links 2026-01-25 19:07:23 -06:00
303 changed files with 14048 additions and 4728 deletions
+83
View File
@@ -0,0 +1,83 @@
name: close-stale-prs
on:
workflow_dispatch:
inputs:
dryRun:
description: "Log actions without closing PRs"
type: boolean
default: false
schedule:
- cron: "0 6 * * *"
permissions:
contents: read
issues: write
pull-requests: write
jobs:
close-stale-prs:
runs-on: ubuntu-latest
steps:
- name: Close inactive PRs
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const DAYS_INACTIVE = 60
const cutoff = new Date(Date.now() - DAYS_INACTIVE * 24 * 60 * 60 * 1000)
const { owner, repo } = context.repo
const dryRun = context.payload.inputs?.dryRun === "true"
const stalePrs = []
core.info(`Dry run mode: ${dryRun}`)
const prs = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: "open",
per_page: 100,
sort: "updated",
direction: "asc",
})
for (const pr of prs) {
const lastUpdated = new Date(pr.updated_at)
if (lastUpdated > cutoff) {
core.info(`PR ${pr.number} is fresh`)
continue
}
stalePrs.push(pr)
}
if (!stalePrs.length) {
core.info("No stale pull requests found.")
return
}
for (const pr of stalePrs) {
const issue_number = pr.number
const closeComment = `Closing this pull request because it has had no updates for more than ${DAYS_INACTIVE} days. If you plan to continue working on it, feel free to reopen or open a new PR.`
if (dryRun) {
core.info(`[dry-run] Would close PR #${issue_number} from ${pr.user.login}`)
continue
}
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body: closeComment,
})
await github.rest.pulls.update({
owner,
repo,
pull_number: issue_number,
state: "closed",
})
core.info(`Closed PR #${issue_number} from ${pr.user.login}`)
}
+1 -1
View File
@@ -1,4 +1,4 @@
name: Daily Issues Recap
name: daily-issues-recap
on:
schedule:
+1 -1
View File
@@ -1,4 +1,4 @@
name: Daily PR Recap
name: daily-pr-recap
on:
schedule:
+1 -1
View File
@@ -1,4 +1,4 @@
name: Docs Update
name: docs-update
on:
schedule:
+1 -1
View File
@@ -1,4 +1,4 @@
name: Duplicate Issue Detection
name: duplicate-issues
on:
issues:
-46
View File
@@ -1,46 +0,0 @@
name: nix desktop
on:
push:
branches: [dev]
paths:
- "flake.nix"
- "flake.lock"
- "nix/**"
- "packages/app/**"
- "packages/desktop/**"
- ".github/workflows/nix-desktop.yml"
pull_request:
paths:
- "flake.nix"
- "flake.lock"
- "nix/**"
- "packages/app/**"
- "packages/desktop/**"
- ".github/workflows/nix-desktop.yml"
workflow_dispatch:
jobs:
build-desktop:
strategy:
fail-fast: false
matrix:
os:
- blacksmith-4vcpu-ubuntu-2404
- blacksmith-4vcpu-ubuntu-2404-arm
- macos-15-intel
- macos-latest
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Nix
uses: nixbuild/nix-quick-install-action@v34
- name: Build desktop via flake
run: |
set -euo pipefail
nix --version
nix build .#desktop -L
@@ -1,4 +1,4 @@
name: Update Nix Hashes
name: nix-hashes
permissions:
contents: write
@@ -11,17 +11,17 @@ on:
- "package.json"
- "packages/*/package.json"
- "flake.lock"
- ".github/workflows/update-nix-hashes.yml"
- ".github/workflows/nix-hashes.yml"
pull_request:
paths:
- "bun.lock"
- "package.json"
- "packages/*/package.json"
- "flake.lock"
- ".github/workflows/update-nix-hashes.yml"
- ".github/workflows/nix-hashes.yml"
jobs:
update-node-modules-hashes:
nix-hashes:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: blacksmith-4vcpu-ubuntu-2404
env:
+1 -1
View File
@@ -1,4 +1,4 @@
name: discord
name: notify-discord
on:
release:
@@ -1,4 +1,4 @@
name: Duplicate PR Check
name: pr-management
on:
pull_request_target:
@@ -63,3 +63,26 @@ jobs:
gh pr comment "$PR_NUMBER" --body "_The following comment was made by an LLM, it may be inaccurate:_
$COMMENT"
add-contributor-label:
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write
steps:
- name: Add Contributor Label
uses: actions/github-script@v8
with:
script: |
const isPR = !!context.payload.pull_request;
const issueNumber = isPR ? context.payload.pull_request.number : context.payload.issue.number;
const authorAssociation = isPR ? context.payload.pull_request.author_association : context.payload.issue.author_association;
if (authorAssociation === 'CONTRIBUTOR') {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: ['contributor']
});
}
+1 -1
View File
@@ -1,4 +1,4 @@
name: PR Standards
name: pr-standards
on:
pull_request_target:
+90 -73
View File
@@ -4,6 +4,7 @@ run-name: "${{ format('release {0}', inputs.bump) }}"
on:
push:
branches:
- ci
- dev
- snapshot-*
workflow_dispatch:
@@ -29,56 +30,46 @@ permissions:
packages: write
jobs:
publish:
version:
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.repository == 'anomalyco/opencode'
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
fetch-depth: 1
- uses: ./.github/actions/setup-bun
- id: version
run: |
./script/version.ts
env:
GH_TOKEN: ${{ github.token }}
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_VERSION: ${{ inputs.version }}
outputs:
version: ${{ steps.version.outputs.version }}
release: ${{ steps.version.outputs.release }}
tag: ${{ steps.version.outputs.tag }}
- run: git fetch --force --tags
build-cli:
needs: version
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.repository == 'anomalyco/opencode'
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 1
fetch-tags: true
- uses: ./.github/actions/setup-bun
- name: Install OpenCode
if: inputs.bump || inputs.version
run: bun i -g opencode-ai@1.0.169
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
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: Setup Git Identity
- name: Build
id: build
run: |
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
git remote set-url origin https://x-access-token:${{ secrets.SST_GITHUB_TOKEN }}@github.com/${{ github.repository }}
- name: Publish
id: publish
run: ./script/publish-start.ts
./packages/opencode/script/build.ts
env:
OPENCODE_BUMP: ${{ inputs.bump }}
OPENCODE_VERSION: ${{ inputs.version }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
AUR_KEY: ${{ secrets.AUR_KEY }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
NPM_CONFIG_PROVENANCE: false
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
GH_TOKEN: ${{ github.token }}
- uses: actions/upload-artifact@v4
with:
@@ -86,12 +77,12 @@ jobs:
path: packages/opencode/dist
outputs:
release: ${{ steps.publish.outputs.release }}
tag: ${{ steps.publish.outputs.tag }}
version: ${{ steps.publish.outputs.version }}
version: ${{ needs.version.outputs.version }}
publish-tauri:
needs: publish
build-tauri:
needs:
- build-cli
- version
continue-on-error: false
strategy:
fail-fast: false
@@ -111,8 +102,8 @@ jobs:
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ needs.publish.outputs.tag }}
fetch-depth: 1
fetch-tags: true
- uses: apple-actions/import-codesign-certs@v2
if: ${{ runner.os == 'macOS' }}
@@ -134,8 +125,6 @@ jobs:
run: |
echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8
- run: git fetch --force --tags
- uses: ./.github/actions/setup-bun
- name: install dependencies (ubuntu only)
@@ -160,10 +149,7 @@ jobs:
bun ./scripts/prepare.ts
env:
OPENCODE_VERSION: ${{ needs.publish.outputs.version }}
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
RUST_TARGET: ${{ matrix.settings.target }}
GH_TOKEN: ${{ github.token }}
GITHUB_RUN_ID: ${{ github.run_id }}
@@ -177,22 +163,18 @@ jobs:
cargo tauri --version
- name: Build and upload artifacts
uses: Wandalen/wretry.action@v3
uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
timeout-minutes: 60
with:
attempt_limit: 3
attempt_delay: 10000
action: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
with: |
projectPath: packages/desktop
uploadWorkflowArtifacts: true
tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
args: --target ${{ matrix.settings.target }} --config ./src-tauri/tauri.prod.conf.json --verbose
updaterJsonPreferNsis: true
releaseId: ${{ needs.publish.outputs.release }}
tagName: ${{ needs.publish.outputs.tag }}
releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
releaseDraft: true
projectPath: packages/desktop
uploadWorkflowArtifacts: true
tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
args: --target ${{ matrix.settings.target }} --config ./src-tauri/tauri.prod.conf.json --verbose
updaterJsonPreferNsis: true
releaseId: ${{ needs.version.outputs.release }}
tagName: ${{ needs.version.outputs.tag }}
releaseDraft: true
releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_BUNDLER_NEW_APPIMAGE_FORMAT: true
@@ -205,20 +187,52 @@ jobs:
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
publish-release:
publish:
needs:
- publish
- publish-tauri
if: needs.publish.outputs.tag
- version
- build-cli
- build-tauri
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ needs.publish.outputs.tag }}
fetch-depth: 1
- name: Install OpenCode
if: inputs.bump || inputs.version
run: bun i -g opencode-ai
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
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: Setup Git Identity
run: |
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
git remote set-url origin https://x-access-token:${{ secrets.SST_GITHUB_TOKEN }}@github.com/${{ github.repository }}
- uses: ./.github/actions/setup-bun
- uses: actions/download-artifact@v4
with:
name: opencode-cli
path: packages/opencode/dist
- name: Setup SSH for AUR
run: |
sudo apt-get update
@@ -230,8 +244,11 @@ jobs:
git config --global user.name "opencode"
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true
- run: ./script/publish-complete.ts
- run: ./script/publish.ts
env:
OPENCODE_VERSION: ${{ needs.publish.outputs.version }}
OPENCODE_VERSION: ${{ needs.version.outputs.version }}
OPENCODE_RELEASE: ${{ needs.version.outputs.release }}
AUR_KEY: ${{ secrets.AUR_KEY }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
NPM_CONFIG_PROVENANCE: false
+1 -1
View File
@@ -1,4 +1,4 @@
name: Guidelines Check
name: review
on:
issue_comment:
+1 -1
View File
@@ -1,4 +1,4 @@
name: "Auto-close stale issues"
name: stale-issues
on:
schedule:
-4
View File
@@ -1,9 +1,6 @@
name: test
on:
push:
branches:
- dev
pull_request:
workflow_dispatch:
jobs:
@@ -20,7 +17,6 @@ jobs:
command: |
git config --global user.email "bot@opencode.ai"
git config --global user.name "opencode"
bun turbo typecheck
bun turbo test
- name: windows
host: windows-latest
+1 -1
View File
@@ -1,4 +1,4 @@
name: Issue Triage
name: triage
on:
issues:
+1 -1
View File
@@ -7,7 +7,7 @@ Please read @package.json and @packages/opencode/package.json.
Your job is to look into AI SDK dependencies, figure out if they have versions that can be upgraded (minor or patch versions ONLY no major ignore major changes).
I want a report of every dependency and the version that can be upgraded to.
What would be even better is if you can give me links to the changelog for each dependency, or at least some reference info so I can see what bugs were fixed or new features were added.
What would be even better is if you can give me brief summary of the changes for each dep and a link to the changelog for each dependency, or at least some reference info so I can see what bugs were fixed or new features were added.
Consider using subagents for each dep to save your context window.
+42
View File
@@ -0,0 +1,42 @@
---
description: Extract non-obvious learnings from session to AGENTS.md files to build codebase understanding
---
Analyze this session and extract non-obvious learnings to add to AGENTS.md files.
AGENTS.md files can exist at any directory level, not just the project root. When an agent reads a file, any AGENTS.md in parent directories are automatically loaded into the context of the tool read. Place learnings as close to the relevant code as possible:
- Project-wide learnings → root AGENTS.md
- Package/module-specific → packages/foo/AGENTS.md
- Feature-specific → src/auth/AGENTS.md
What counts as a learning (non-obvious discoveries only):
- Hidden relationships between files or modules
- Execution paths that differ from how code appears
- Non-obvious configuration, env vars, or flags
- Debugging breakthroughs when error messages were misleading
- API/tool quirks and workarounds
- Build/test commands not in README
- Architectural decisions and constraints
- Files that must change together
What NOT to include:
- Obvious facts from documentation
- Standard language/framework behavior
- Things already in an AGENTS.md
- Verbose explanations
- Session-specific details
Process:
1. Review session for discoveries, errors that took multiple attempts, unexpected connections
2. Determine scope - what directory does each learning apply to?
3. Read existing AGENTS.md files at relevant levels
4. Create or update AGENTS.md at the appropriate level
5. Keep entries to 1-3 lines per insight
After updating, summarize which AGENTS.md files were created/updated and how many learnings per file.
$ARGUMENTS
+1
View File
@@ -10,6 +10,7 @@
- Avoid using the `any` type
- Prefer single word variable names where possible
- Use Bun APIs when possible, like `Bun.file()`
- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity
### Avoid let statements
+14 -13
View File
@@ -15,20 +15,21 @@
</p>
<p align="center">
<a href="README.md">الإنجليزية</a> |
<a href="README.zh.md">الصينية (المبسطة)</a> |
<a href="README.zht.md">الصينية (التقليدية)</a> |
<a href="README.ko.md">الكورية</a> |
<a href="README.de.md">الألمانية</a> |
<a href="README.es.md">الإسبانية</a> |
<a href="README.fr.md">الفرنسية</a> |
<a href="README.da.md">الدنماركية</a> |
<a href="README.ja.md">اليابانية</a> |
<a href="README.pl.md">البولندية</a> |
<a href="README.ru.md">الروسية</a> |
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.it.md">Italiano</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">النرويجية</a> |
<a href="README.br.md">البرتغالية (البرازيل)</a>
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+14 -13
View File
@@ -15,19 +15,20 @@
</p>
<p align="center">
<a href="README.md">Inglês</a> |
<a href="README.zh.md">Chinês (Simplificado)</a> |
<a href="README.zht.md">Chinês (Tradicional)</a> |
<a href="README.ko.md">Coreano</a> |
<a href="README.de.md">Alemão</a> |
<a href="README.es.md">Espanhol</a> |
<a href="README.fr.md">Frans</a> |
<a href="README.da.md">Dinamarquês</a> |
<a href="README.ja.md">Japonês</a> |
<a href="README.pl.md">Polonês</a> |
<a href="README.ru.md">Russo</a> |
<a href="README.ar.md">Árabe</a> |
<a href="README.no.md">Norueguês</a> |
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.it.md">Italiano</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
</p>
+13 -12
View File
@@ -15,20 +15,21 @@
</p>
<p align="center">
<a href="README.md">Engelsk</a> |
<a href="README.zh.md">Kinesisk (forenklet)</a> |
<a href="README.zht.md">Kinesisk (traditionelt)</a> |
<a href="README.ko.md">Koreansk</a> |
<a href="README.de.md">Tysk</a> |
<a href="README.es.md">Spansk</a> |
<a href="README.fr.md">Fransk</a> |
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.it.md">Italiano</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">Japansk</a> |
<a href="README.pl.md">Polsk</a> |
<a href="README.ru.md">Russisk</a> |
<a href="README.ar.md">Arabisk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Portugisisk (Brasilien)</a>
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+14 -13
View File
@@ -15,20 +15,21 @@
</p>
<p align="center">
<a href="README.md">Englisch</a> |
<a href="README.zh.md">Chinesisch (vereinfacht)</a> |
<a href="README.zht.md">Chinesisch (traditionell)</a> |
<a href="README.ko.md">Koreanisch</a> |
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Spanisch</a> |
<a href="README.fr.md">Französisch</a> |
<a href="README.da.md">Dänisch</a> |
<a href="README.ja.md">Japanisch</a> |
<a href="README.pl.md">Polnisch</a> |
<a href="README.ru.md">Russisch</a> |
<a href="README.ar.md">Arabisch</a> |
<a href="README.no.md">Norwegisch</a> |
<a href="README.br.md">Portugiesisch (Brasilien)</a>
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.it.md">Italiano</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+14 -13
View File
@@ -15,20 +15,21 @@
</p>
<p align="center">
<a href="README.md">Inglés</a> |
<a href="README.zh.md">Chino (simplificado)</a> |
<a href="README.zht.md">Chino (tradicional)</a> |
<a href="README.ko.md">Coreano</a> |
<a href="README.de.md">Alemán</a> |
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Frans</a> |
<a href="README.da.md">Danés</a> |
<a href="README.ja.md">Japonés</a> |
<a href="README.pl.md">Polaco</a> |
<a href="README.ru.md">Ruso</a> |
<a href="README.ar.md">Árabe</a> |
<a href="README.no.md">Noruego</a> |
<a href="README.br.md">Portugués (Brasil)</a>
<a href="README.fr.md">Français</a> |
<a href="README.it.md">Italiano</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+14 -13
View File
@@ -15,20 +15,21 @@
</p>
<p align="center">
<a href="README.md">Anglais</a> |
<a href="README.zh.md">Chinois (simplifié)</a> |
<a href="README.zht.md">Chinois (traditionnel)</a> |
<a href="README.ko.md">Coréen</a> |
<a href="README.de.md">Allemand</a> |
<a href="README.es.md">Espagnol</a> |
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.da.md">Danois</a> |
<a href="README.ja.md">Japonais</a> |
<a href="README.pl.md">Polonais</a> |
<a href="README.ru.md">Russe</a> |
<a href="README.ar.md">Arabe</a> |
<a href="README.no.md">Norvégien</a> |
<a href="README.br.md">Portugais (Brésil)</a>
<a href="README.it.md">Italiano</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+133
View File
@@ -0,0 +1,133 @@
<p align="center">
<a href="https://opencode.ai">
<picture>
<source srcset="packages/console/app/src/asset/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
<source srcset="packages/console/app/src/asset/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
<img src="packages/console/app/src/asset/logo-ornate-light.svg" alt="Logo OpenCode">
</picture>
</a>
</p>
<p align="center">Lagente di coding AI open source.</p>
<p align="center">
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
<a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
</p>
<p align="center">
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.it.md">Italiano</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
---
### Installazione
```bash
# YOLO
curl -fsSL https://opencode.ai/install | bash
# Package manager
npm i -g opencode-ai@latest # oppure bun/pnpm/yarn
scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS e Linux (consigliato, sempre aggiornato)
brew install opencode # macOS e Linux (formula brew ufficiale, aggiornata meno spesso)
paru -S opencode-bin # Arch Linux
mise use -g opencode # Qualsiasi OS
nix run nixpkgs#opencode # oppure github:anomalyco/opencode per lultima branch di sviluppo
```
> [!TIP]
> Rimuovi le versioni precedenti alla 0.1.x prima di installare.
### App Desktop (BETA)
OpenCode è disponibile anche come applicazione desktop. Puoi scaricarla direttamente dalla [pagina delle release](https://github.com/anomalyco/opencode/releases) oppure da [opencode.ai/download](https://opencode.ai/download).
| Piattaforma | Download |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, oppure AppImage |
```bash
# macOS (Homebrew)
brew install --cask opencode-desktop
# Windows (Scoop)
scoop bucket add extras; scoop install extras/opencode-desktop
```
#### Directory di installazione
Lo script di installazione rispetta il seguente ordine di priorità per il percorso di installazione:
1. `$OPENCODE_INSTALL_DIR` Directory di installazione personalizzata
2. `$XDG_BIN_DIR` Percorso conforme alla XDG Base Directory Specification
3. `$HOME/bin` Directory binaria standard dellutente (se esiste o può essere creata)
4. `$HOME/.opencode/bin` Fallback predefinito
```bash
# Esempi
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
```
### Agenti
OpenCode include due agenti integrati tra cui puoi passare usando il tasto `Tab`.
- **build** Predefinito, agente con accesso completo per il lavoro di sviluppo
- **plan** Agente in sola lettura per analisi ed esplorazione del codice
- Nega le modifiche ai file per impostazione predefinita
- Chiede il permesso prima di eseguire comandi bash
- Ideale per esplorare codebase sconosciute o pianificare modifiche
È inoltre incluso un sotto-agente **general** per ricerche complesse e attività multi-step.
Viene utilizzato internamente e può essere invocato usando `@general` nei messaggi.
Scopri di più sugli [agenti](https://opencode.ai/docs/agents).
### Documentazione
Per maggiori informazioni su come configurare OpenCode, [**consulta la nostra documentazione**](https://opencode.ai/docs).
### Contribuire
Se sei interessato a contribuire a OpenCode, leggi la nostra [guida alla contribuzione](./CONTRIBUTING.md) prima di inviare una pull request.
### Costruire su OpenCode
Se stai lavorando a un progetto correlato a OpenCode e che utilizza “opencode” come parte del nome (ad esempio “opencode-dashboard” o “opencode-mobile”), aggiungi una nota nel tuo README per chiarire che non è sviluppato dal team OpenCode e che non è affiliato in alcun modo con noi.
### FAQ
#### In cosa è diverso da Claude Code?
È molto simile a Claude Code in termini di funzionalità. Ecco le principali differenze:
- 100% open source
- Non è legato a nessun provider. Anche se consigliamo i modelli forniti tramite [OpenCode Zen](https://opencode.ai/zen), OpenCode può essere utilizzato con Claude, OpenAI, Google o persino modelli locali. Con levoluzione dei modelli, le differenze tra di essi si ridurranno e i prezzi scenderanno, quindi essere indipendenti dal provider è importante.
- Supporto LSP pronto alluso
- Forte attenzione alla TUI. OpenCode è sviluppato da utenti neovim e dai creatori di [terminal.shop](https://terminal.shop); spingeremo al limite ciò che è possibile fare nel terminale.
- Architettura client/server. Questo, ad esempio, permette a OpenCode di girare sul tuo computer mentre lo controlli da remoto tramite unapp mobile. La frontend TUI è quindi solo uno dei possibili client.
---
**Unisciti alla nostra community** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
+14 -13
View File
@@ -15,20 +15,21 @@
</p>
<p align="center">
<a href="README.md">英語</a> |
<a href="README.zh.md">中国語(簡体字)</a> |
<a href="README.zht.md">中国語(繁体字)</a> |
<a href="README.ko.md">韓国語</a> |
<a href="README.de.md">ドイツ語</a> |
<a href="README.es.md">スペイン語</a> |
<a href="README.fr.md">フランス語</a> |
<a href="README.da.md">デンマーク語</a> |
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.it.md">Italiano</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">ポーランド語</a> |
<a href="README.ru.md">ロシア語</a> |
<a href="README.ar.md">アラビア語</a> |
<a href="README.no.md">ノルウェー語</a> |
<a href="README.br.md">ポルトガル語(ブラジル)</a>
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+14 -13
View File
@@ -15,20 +15,21 @@
</p>
<p align="center">
<a href="README.md">영어</a> |
<a href="README.zh.md">중국어(간체)</a> |
<a href="README.zht.md">중국어(번체)</a> |
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">독일어</a> |
<a href="README.es.md">스페인어</a> |
<a href="README.fr.md">프랑스어</a> |
<a href="README.da.md">덴마크어</a> |
<a href="README.ja.md">일본어</a> |
<a href="README.pl.md">폴란드어</a> |
<a href="README.ru.md">러시아어</a> |
<a href="README.ar.md">아랍어</a> |
<a href="README.no.md">노르웨이어</a> |
<a href="README.br.md">포르투갈어(브라질)</a>
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.it.md">Italiano</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+15 -13
View File
@@ -16,19 +16,21 @@
<p align="center">
<a href="README.md">English</a> |
<a href="README.zh.md">Chinese (Simplified)</a> |
<a href="README.zht.md">Chinese (Traditional)</a> |
<a href="README.ko.md">Korean</a> |
<a href="README.de.md">German</a> |
<a href="README.es.md">Spanish</a> |
<a href="README.fr.md">French</a> |
<a href="README.da.md">Danish</a> |
<a href="README.ja.md">Japanese</a> |
<a href="README.pl.md">Polish</a> |
<a href="README.ru.md">Russian</a> |
<a href="README.ar.md">Arabic</a> |
<a href="README.no.md">Norwegian</a> |
<a href="README.br.md">Portuguese (Brazil)</a>
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.it.md">Italiano</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a> |
<a href="README.th.md">ไทย</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+13 -12
View File
@@ -15,20 +15,21 @@
</p>
<p align="center">
<a href="README.md">Engelsk</a> |
<a href="README.zh.md">Kinesisk (forenklet)</a> |
<a href="README.zht.md">Kinesisk (tradisjonell)</a> |
<a href="README.ko.md">Koreansk</a> |
<a href="README.de.md">Tysk</a> |
<a href="README.es.md">Spansk</a> |
<a href="README.fr.md">Fransk</a> |
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.it.md">Italiano</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">Japansk</a> |
<a href="README.pl.md">Polsk</a> |
<a href="README.ru.md">Russisk</a> |
<a href="README.ar.md">Arabisk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Portugisisk (Brasil)</a>
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+14 -13
View File
@@ -15,20 +15,21 @@
</p>
<p align="center">
<a href="README.md">Angielski</a> |
<a href="README.zh.md">Chiński (uproszczony)</a> |
<a href="README.zht.md">Chiński (tradycyjny)</a> |
<a href="README.ko.md">Koreański</a> |
<a href="README.de.md">Niemiecki</a> |
<a href="README.es.md">Hiszpański</a> |
<a href="README.fr.md">Francuski</a> |
<a href="README.da.md">Duński</a> |
<a href="README.ja.md">Japoński</a> |
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.it.md">Italiano</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Rosyjski</a> |
<a href="README.ar.md">Arabski</a> |
<a href="README.no.md">Norweski</a> |
<a href="README.br.md">Portugalski (Brazylia)</a>
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+14 -13
View File
@@ -15,20 +15,21 @@
</p>
<p align="center">
<a href="README.md">Английский</a> |
<a href="README.zh.md">Китайский (упрощенный)</a> |
<a href="README.zht.md">Китайский (традиционный)</a> |
<a href="README.ko.md">Корейский</a> |
<a href="README.de.md">Немецкий</a> |
<a href="README.es.md">Испанский</a> |
<a href="README.fr.md">Французский</a> |
<a href="README.da.md">Датский</a> |
<a href="README.ja.md">Японский</a> |
<a href="README.pl.md">Польский</a> |
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.it.md">Italiano</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">Арабский</a> |
<a href="README.no.md">Норвежский</a> |
<a href="README.br.md">Португальский (Бразилия)</a>
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+134
View File
@@ -0,0 +1,134 @@
<p align="center">
<a href="https://opencode.ai">
<picture>
<source srcset="packages/console/app/src/asset/logo-ornate-dark.svg" media="(prefers-color-scheme: dark)">
<source srcset="packages/console/app/src/asset/logo-ornate-light.svg" media="(prefers-color-scheme: light)">
<img src="packages/console/app/src/asset/logo-ornate-light.svg" alt="OpenCode logo">
</picture>
</a>
</p>
<p align="center">เอเจนต์การเขียนโค้ดด้วย AI แบบโอเพนซอร์ส</p>
<p align="center">
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
<a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="สถานะการสร้าง" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
</p>
<p align="center">
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.it.md">Italiano</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a> |
<a href="README.th.md">ไทย</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
---
### การติดตั้ง
```bash
# YOLO
curl -fsSL https://opencode.ai/install | bash
# ตัวจัดการแพ็กเกจ
npm i -g opencode-ai@latest # หรือ bun/pnpm/yarn
scoop install opencode # Windows
choco install opencode # Windows
brew install anomalyco/tap/opencode # macOS และ Linux (แนะนำ อัปเดตเสมอ)
brew install opencode # macOS และ Linux (brew formula อย่างเป็นทางการ อัปเดตน้อยกว่า)
paru -S opencode-bin # Arch Linux
mise use -g opencode # ระบบปฏิบัติการใดก็ได้
nix run nixpkgs#opencode # หรือ github:anomalyco/opencode สำหรับสาขาพัฒนาล่าสุด
```
> [!TIP]
> ลบเวอร์ชันที่เก่ากว่า 0.1.x ก่อนติดตั้ง
### แอปพลิเคชันเดสก์ท็อป (เบต้า)
OpenCode มีให้ใช้งานเป็นแอปพลิเคชันเดสก์ท็อป ดาวน์โหลดโดยตรงจาก [หน้ารุ่น](https://github.com/anomalyco/opencode/releases) หรือ [opencode.ai/download](https://opencode.ai/download)
| แพลตฟอร์ม | ดาวน์โหลด |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, หรือ AppImage |
```bash
# macOS (Homebrew)
brew install --cask opencode-desktop
# Windows (Scoop)
scoop bucket add extras; scoop install extras/opencode-desktop
```
#### ไดเรกทอรีการติดตั้ง
สคริปต์การติดตั้งจะใช้ลำดับความสำคัญตามเส้นทางการติดตั้ง:
1. `$OPENCODE_INSTALL_DIR` - ไดเรกทอรีการติดตั้งที่กำหนดเอง
2. `$XDG_BIN_DIR` - เส้นทางที่สอดคล้องกับ XDG Base Directory Specification
3. `$HOME/bin` - ไดเรกทอรีไบนารีผู้ใช้มาตรฐาน (หากมีอยู่หรือสามารถสร้างได้)
4. `$HOME/.opencode/bin` - ค่าสำรองเริ่มต้น
```bash
# ตัวอย่าง
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
```
### เอเจนต์
OpenCode รวมเอเจนต์ในตัวสองตัวที่คุณสามารถสลับได้ด้วยปุ่ม `Tab`
- **build** - เอเจนต์เริ่มต้น มีสิทธิ์เข้าถึงแบบเต็มสำหรับงานพัฒนา
- **plan** - เอเจนต์อ่านอย่างเดียวสำหรับการวิเคราะห์และการสำรวจโค้ด
- ปฏิเสธการแก้ไขไฟล์โดยค่าเริ่มต้น
- ขอสิทธิ์ก่อนเรียกใช้คำสั่ง bash
- เหมาะสำหรับสำรวจโค้ดเบสที่ไม่คุ้นเคยหรือวางแผนการเปลี่ยนแปลง
นอกจากนี้ยังมีเอเจนต์ย่อย **general** สำหรับการค้นหาที่ซับซ้อนและงานหลายขั้นตอน
ใช้ภายในและสามารถเรียกใช้ได้โดยใช้ `@general` ในข้อความ
เรียนรู้เพิ่มเติมเกี่ยวกับ [เอเจนต์](https://opencode.ai/docs/agents)
### เอกสารประกอบ
สำหรับข้อมูลเพิ่มเติมเกี่ยวกับวิธีกำหนดค่า OpenCode [**ไปที่เอกสารของเรา**](https://opencode.ai/docs)
### การมีส่วนร่วม
หากคุณสนใจที่จะมีส่วนร่วมใน OpenCode โปรดอ่าน [เอกสารการมีส่วนร่วม](./CONTRIBUTING.md) ก่อนส่ง Pull Request
### การสร้างบน OpenCode
หากคุณทำงานในโปรเจกต์ที่เกี่ยวข้องกับ OpenCode และใช้ "opencode" เป็นส่วนหนึ่งของชื่อ เช่น "opencode-dashboard" หรือ "opencode-mobile" โปรดเพิ่มหมายเหตุใน README ของคุณเพื่อชี้แจงว่าไม่ได้สร้างโดยทีม OpenCode และไม่ได้เกี่ยวข้องกับเราในทางใด
### คำถามที่พบบ่อย
#### ต่างจาก Claude Code อย่างไร?
คล้ายกับ Claude Code มากในแง่ความสามารถ นี่คือความแตกต่างหลัก:
- โอเพนซอร์ส 100%
- ไม่ผูกมัดกับผู้ให้บริการใดๆ แม้ว่าเราจะแนะนำโมเดลที่เราจัดหาให้ผ่าน [OpenCode Zen](https://opencode.ai/zen) OpenCode สามารถใช้กับ Claude, OpenAI, Google หรือแม้กระทั่งโมเดลในเครื่องได้ เมื่อโมเดลพัฒนาช่องว่างระหว่างพวกมันจะปิดลงและราคาจะลดลง ดังนั้นการไม่ผูกมัดกับผู้ให้บริการจึงสำคัญ
- รองรับ LSP ใช้งานได้ทันทีหลังการติดตั้งโดยไม่ต้องปรับแต่งหรือเปลี่ยนแปลงฟังก์ชันการทำงานใด ๆ
- เน้นที่ TUI OpenCode สร้างโดยผู้ใช้ neovim และผู้สร้าง [terminal.shop](https://terminal.shop) เราจะผลักดันขีดจำกัดของสิ่งที่เป็นไปได้ในเทอร์มินัล
- สถาปัตยกรรมไคลเอนต์/เซิร์ฟเวอร์ ตัวอย่างเช่น อาจอนุญาตให้ OpenCode ทำงานบนคอมพิวเตอร์ของคุณ ในขณะที่คุณสามารถขับเคลื่อนจากระยะไกลผ่านแอปมือถือ หมายความว่า TUI frontend เป็นหนึ่งในไคลเอนต์ที่เป็นไปได้เท่านั้น
---
**ร่วมชุมชนของเรา** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
+14 -13
View File
@@ -15,20 +15,21 @@
</p>
<p align="center">
<a href="README.md">英语</a> |
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁中文</a> |
<a href="README.ko.md">韩语</a> |
<a href="README.de.md">德语</a> |
<a href="README.es.md">西班牙语</a> |
<a href="README.fr.md">法语</a> |
<a href="README.da.md">丹麦语</a> |
<a href="README.ja.md">日语</a> |
<a href="README.pl.md">波兰语</a> |
<a href="README.ru.md">俄语</a> |
<a href="README.ar.md">阿拉伯语</a> |
<a href="README.no.md">挪威语</a> |
<a href="README.br.md">葡萄牙语(巴西)</a>
<a href="README.zht.md">繁中文</a> |
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.it.md">Italiano</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+14 -13
View File
@@ -15,20 +15,21 @@
</p>
<p align="center">
<a href="README.md">英語</a> |
<a href="README.zh.md">簡體中文</a> |
<a href="README.md">English</a> |
<a href="README.zh.md">简体中文</a> |
<a href="README.zht.md">繁體中文</a> |
<a href="README.ko.md">韓語</a> |
<a href="README.de.md">德語</a> |
<a href="README.es.md">西班牙語</a> |
<a href="README.fr.md">法語</a> |
<a href="README.da.md">丹麥語</a> |
<a href="README.ja.md">日語</a> |
<a href="README.pl.md">波蘭語</a> |
<a href="README.ru.md">俄語</a> |
<a href="README.ar.md">阿拉伯語</a> |
<a href="README.no.md">挪威語</a> |
<a href="README.br.md">葡萄牙語(巴西)</a>
<a href="README.ko.md">한국어</a> |
<a href="README.de.md">Deutsch</a> |
<a href="README.es.md">Español</a> |
<a href="README.fr.md">Français</a> |
<a href="README.it.md">Italiano</a> |
<a href="README.da.md">Dansk</a> |
<a href="README.ja.md">日本語</a> |
<a href="README.pl.md">Polski</a> |
<a href="README.ru.md">Русский</a> |
<a href="README.ar.md">العربية</a> |
<a href="README.no.md">Norsk</a> |
<a href="README.br.md">Português (Brasil)</a>
</p>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
+215 -211
View File
@@ -1,213 +1,217 @@
# 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) |
| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) |
| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) |
| 2025-12-15 | 1,093,632 (+11,590) | 1,059,078 (+6,653) | 2,152,710 (+18,243) |
| 2025-12-16 | 1,120,477 (+26,845) | 1,078,022 (+18,944) | 2,198,499 (+45,789) |
| 2025-12-17 | 1,151,067 (+30,590) | 1,097,661 (+19,639) | 2,248,728 (+50,229) |
| 2025-12-18 | 1,178,658 (+27,591) | 1,113,418 (+15,757) | 2,292,076 (+43,348) |
| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) |
| 2025-12-20 | 1,223,000 (+19,515) | 1,146,258 (+16,560) | 2,369,258 (+36,075) |
| 2025-12-21 | 1,242,675 (+19,675) | 1,158,909 (+12,651) | 2,401,584 (+32,326) |
| 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) |
| 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) |
| 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) |
| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) |
| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) |
| 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) |
| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) |
| 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) |
| 2025-12-30 | 1,445,450 (+29,890) | 1,272,689 (+15,588) | 2,718,139 (+45,478) |
| 2025-12-31 | 1,479,598 (+34,148) | 1,293,235 (+20,546) | 2,772,833 (+54,694) |
| 2026-01-01 | 1,508,883 (+29,285) | 1,309,874 (+16,639) | 2,818,757 (+45,924) |
| 2026-01-02 | 1,563,474 (+54,591) | 1,320,959 (+11,085) | 2,884,433 (+65,676) |
| 2026-01-03 | 1,618,065 (+54,591) | 1,331,914 (+10,955) | 2,949,979 (+65,546) |
| 2026-01-04 | 1,672,656 (+39,702) | 1,339,883 (+7,969) | 3,012,539 (+62,560) |
| 2026-01-05 | 1,738,171 (+65,515) | 1,353,043 (+13,160) | 3,091,214 (+78,675) |
| 2026-01-06 | 1,960,988 (+222,817) | 1,377,377 (+24,334) | 3,338,365 (+247,151) |
| 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) |
| 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) |
| 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971) | 3,913,016 (+207,906) |
| 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) |
| 2026-01-11 | 2,836,394 (+204,371) | 1,530,479 (+26,809) | 4,366,873 (+231,180) |
| 2026-01-12 | 3,053,594 (+217,200) | 1,553,671 (+23,192) | 4,607,265 (+240,392) |
| 2026-01-13 | 3,297,078 (+243,484) | 1,595,062 (+41,391) | 4,892,140 (+284,875) |
| 2026-01-14 | 3,568,928 (+271,850) | 1,645,362 (+50,300) | 5,214,290 (+322,150) |
| 2026-01-16 | 4,121,550 (+552,622) | 1,754,418 (+109,056) | 5,875,968 (+661,678) |
| 2026-01-17 | 4,389,558 (+268,008) | 1,805,315 (+50,897) | 6,194,873 (+318,905) |
| 2026-01-18 | 4,627,623 (+238,065) | 1,839,171 (+33,856) | 6,466,794 (+271,921) |
| 2026-01-19 | 4,861,108 (+233,485) | 1,863,112 (+23,941) | 6,724,220 (+257,426) |
| 2026-01-20 | 5,128,999 (+267,891) | 1,903,665 (+40,553) | 7,032,664 (+308,444) |
| 2026-01-21 | 5,444,842 (+315,843) | 1,962,531 (+58,866) | 7,407,373 (+374,709) |
| 2026-01-22 | 5,766,340 (+321,498) | 2,029,487 (+66,956) | 7,795,827 (+388,454) |
| 2026-01-23 | 6,096,236 (+329,896) | 2,096,235 (+66,748) | 8,192,471 (+396,644) |
| 2026-01-24 | 6,371,019 (+274,783) | 2,156,870 (+60,635) | 8,527,889 (+335,418) |
| 2026-01-25 | 6,639,082 (+268,063) | 2,187,853 (+30,983) | 8,826,935 (+299,046) |
| 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) |
| 2025-12-15 | 1,093,632 (+11,590) | 1,059,078 (+6,653) | 2,152,710 (+18,243) |
| 2025-12-16 | 1,120,477 (+26,845) | 1,078,022 (+18,944) | 2,198,499 (+45,789) |
| 2025-12-17 | 1,151,067 (+30,590) | 1,097,661 (+19,639) | 2,248,728 (+50,229) |
| 2025-12-18 | 1,178,658 (+27,591) | 1,113,418 (+15,757) | 2,292,076 (+43,348) |
| 2025-12-19 | 1,203,485 (+24,827) | 1,129,698 (+16,280) | 2,333,183 (+41,107) |
| 2025-12-20 | 1,223,000 (+19,515) | 1,146,258 (+16,560) | 2,369,258 (+36,075) |
| 2025-12-21 | 1,242,675 (+19,675) | 1,158,909 (+12,651) | 2,401,584 (+32,326) |
| 2025-12-22 | 1,262,522 (+19,847) | 1,169,121 (+10,212) | 2,431,643 (+30,059) |
| 2025-12-23 | 1,286,548 (+24,026) | 1,186,439 (+17,318) | 2,472,987 (+41,344) |
| 2025-12-24 | 1,309,323 (+22,775) | 1,203,767 (+17,328) | 2,513,090 (+40,103) |
| 2025-12-25 | 1,333,032 (+23,709) | 1,217,283 (+13,516) | 2,550,315 (+37,225) |
| 2025-12-26 | 1,352,411 (+19,379) | 1,227,615 (+10,332) | 2,580,026 (+29,711) |
| 2025-12-27 | 1,371,771 (+19,360) | 1,238,236 (+10,621) | 2,610,007 (+29,981) |
| 2025-12-28 | 1,390,388 (+18,617) | 1,245,690 (+7,454) | 2,636,078 (+26,071) |
| 2025-12-29 | 1,415,560 (+25,172) | 1,257,101 (+11,411) | 2,672,661 (+36,583) |
| 2025-12-30 | 1,445,450 (+29,890) | 1,272,689 (+15,588) | 2,718,139 (+45,478) |
| 2025-12-31 | 1,479,598 (+34,148) | 1,293,235 (+20,546) | 2,772,833 (+54,694) |
| 2026-01-01 | 1,508,883 (+29,285) | 1,309,874 (+16,639) | 2,818,757 (+45,924) |
| 2026-01-02 | 1,563,474 (+54,591) | 1,320,959 (+11,085) | 2,884,433 (+65,676) |
| 2026-01-03 | 1,618,065 (+54,591) | 1,331,914 (+10,955) | 2,949,979 (+65,546) |
| 2026-01-04 | 1,672,656 (+39,702) | 1,339,883 (+7,969) | 3,012,539 (+62,560) |
| 2026-01-05 | 1,738,171 (+65,515) | 1,353,043 (+13,160) | 3,091,214 (+78,675) |
| 2026-01-06 | 1,960,988 (+222,817) | 1,377,377 (+24,334) | 3,338,365 (+247,151) |
| 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) |
| 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) |
| 2026-01-09 | 2,443,565 (+170,935) | 1,469,451 (+36,971) | 3,913,016 (+207,906) |
| 2026-01-10 | 2,632,023 (+188,458) | 1,503,670 (+34,219) | 4,135,693 (+222,677) |
| 2026-01-11 | 2,836,394 (+204,371) | 1,530,479 (+26,809) | 4,366,873 (+231,180) |
| 2026-01-12 | 3,053,594 (+217,200) | 1,553,671 (+23,192) | 4,607,265 (+240,392) |
| 2026-01-13 | 3,297,078 (+243,484) | 1,595,062 (+41,391) | 4,892,140 (+284,875) |
| 2026-01-14 | 3,568,928 (+271,850) | 1,645,362 (+50,300) | 5,214,290 (+322,150) |
| 2026-01-16 | 4,121,550 (+552,622) | 1,754,418 (+109,056) | 5,875,968 (+661,678) |
| 2026-01-17 | 4,389,558 (+268,008) | 1,805,315 (+50,897) | 6,194,873 (+318,905) |
| 2026-01-18 | 4,627,623 (+238,065) | 1,839,171 (+33,856) | 6,466,794 (+271,921) |
| 2026-01-19 | 4,861,108 (+233,485) | 1,863,112 (+23,941) | 6,724,220 (+257,426) |
| 2026-01-20 | 5,128,999 (+267,891) | 1,903,665 (+40,553) | 7,032,664 (+308,444) |
| 2026-01-21 | 5,444,842 (+315,843) | 1,962,531 (+58,866) | 7,407,373 (+374,709) |
| 2026-01-22 | 5,766,340 (+321,498) | 2,029,487 (+66,956) | 7,795,827 (+388,454) |
| 2026-01-23 | 6,096,236 (+329,896) | 2,096,235 (+66,748) | 8,192,471 (+396,644) |
| 2026-01-24 | 6,371,019 (+274,783) | 2,156,870 (+60,635) | 8,527,889 (+335,418) |
| 2026-01-25 | 6,639,082 (+268,063) | 2,187,853 (+30,983) | 8,826,935 (+299,046) |
| 2026-01-26 | 6,941,620 (+302,538) | 2,232,115 (+44,262) | 9,173,735 (+346,800) |
| 2026-01-27 | 7,208,093 (+266,473) | 2,280,762 (+48,647) | 9,488,855 (+315,120) |
| 2026-01-28 | 7,489,370 (+281,277) | 2,314,849 (+34,087) | 9,804,219 (+315,364) |
| 2026-01-29 | 7,815,471 (+326,101) | 2,374,982 (+60,133) | 10,190,453 (+386,234) |
+29 -25
View File
@@ -23,7 +23,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.1.36",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -73,7 +73,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.1.36",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -107,7 +107,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.1.36",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -134,7 +134,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.1.36",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -158,7 +158,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.1.36",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -182,12 +182,14 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.1.36",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
"@solid-primitives/i18n": "2.2.1",
"@solid-primitives/storage": "catalog:",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-deep-link": "~2",
"@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-http": "~2",
"@tauri-apps/plugin-notification": "~2",
@@ -211,7 +213,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.1.36",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -240,7 +242,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.1.36",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -256,7 +258,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.1.36",
"version": "0.0.0-ci-202601291718",
"bin": {
"opencode": "./bin/opencode",
},
@@ -296,8 +298,8 @@
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "1.5.2",
"@opentui/core": "0.1.74",
"@opentui/solid": "0.1.74",
"@opentui/core": "0.1.75",
"@opentui/solid": "0.1.75",
"@parcel/watcher": "2.5.1",
"@pierre/diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
@@ -360,7 +362,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.1.36",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -380,7 +382,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.1.36",
"version": "0.0.0-ci-202601291718",
"devDependencies": {
"@hey-api/openapi-ts": "0.90.10",
"@tsconfig/node22": "catalog:",
@@ -391,7 +393,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.1.36",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -404,7 +406,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.1.36",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -446,7 +448,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "1.1.36",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"zod": "catalog:",
},
@@ -457,7 +459,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.1.36",
"version": "0.0.0-ci-202601291718",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -1227,21 +1229,21 @@
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@opentui/core": ["@opentui/core@0.1.74", "", { "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.74", "@opentui/core-darwin-x64": "0.1.74", "@opentui/core-linux-arm64": "0.1.74", "@opentui/core-linux-x64": "0.1.74", "@opentui/core-win32-arm64": "0.1.74", "@opentui/core-win32-x64": "0.1.74", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-g4W16ymv12JdgZ+9B4t7mpIICvzWy2+eHERfmDf80ALduOQCUedKQdULcBFhVCYUXIkDRtIy6CID5thMAah3FA=="],
"@opentui/core": ["@opentui/core@0.1.75", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.75", "@opentui/core-darwin-x64": "0.1.75", "@opentui/core-linux-arm64": "0.1.75", "@opentui/core-linux-x64": "0.1.75", "@opentui/core-win32-arm64": "0.1.75", "@opentui/core-win32-x64": "0.1.75", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-8ARRZxSG+BXkJmEVtM2DQ4se7DAF1ZCKD07d+AklgTr2mxCzmdxxPbOwRzboSQ6FM7qGuTVPVbV4O2W9DpUmoA=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.74", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rfmlDLtm/u17CnuhJgCxPeYMvOST+A2MOdVOk46IurtHO849bdYqK6iudKNlFRs1FOrymgSKF9GlWBHAOKeRjg=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.75", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gGaGZjkFpqcXJk6321JzhRl66pM2VxBlI470L8W4DQUW4S6iDT1R9L7awSzGB4Cn9toUl7DTV8BemaXZYXV4SA=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.74", "", { "os": "darwin", "cpu": "x64" }, "sha512-WAD8orsDV0ZdW/5GwjOOB4FY96772xbkz+rcV7WRzEFUVaqoBaC04IuqYzS9d5s+cjkbT5Cpj47hrVYkkVQKng=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.75", "", { "os": "darwin", "cpu": "x64" }, "sha512-tPlvqQI0whZ76amHydpJs5kN+QeWAIcFbI8RAtlAo9baj2EbxTDC+JGwgb9Fnt0/YQx831humbtaNDhV2Jt1bw=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.74", "", { "os": "linux", "cpu": "arm64" }, "sha512-lgmHzrzLy4e+rgBS+lhtsMLLgIMLbtLNMm6EzVPyYVDlLDGjM7+ulXMem7AtpaRrWrUUl4REiG9BoQUsCFDwYA=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.75", "", { "os": "linux", "cpu": "arm64" }, "sha512-nVxIQ4Hqf84uBergDpWiVzU6pzpjy6tqBHRQpySxZ2flkJ/U6/aMEizVrQ1jcgIdxZtvqWDETZhzxhG0yDx+cw=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.74", "", { "os": "linux", "cpu": "x64" }, "sha512-8Mn2WbdBQ29xCThuPZezjDhd1N3+fXwKkGvCBOdTI0le6h2A/vCNbfUVjwfr/EGZSRXxCG+Yapol34BAULGpOA=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.75", "", { "os": "linux", "cpu": "x64" }, "sha512-1CnApef4kxA+ORyLfbuCLgZfEjp4wr3HjFnt7FAfOb73kIZH82cb7JYixeqRyy9eOcKfKqxLmBYy3o8IDkc4Rg=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.74", "", { "os": "win32", "cpu": "arm64" }, "sha512-dvYUXz03avnI6ZluyLp00HPmR0UT/IE/6QS97XBsgJlUTtpnbKkBtB5jD1NHwWkElaRj1Qv2QP36ngFoJqbl9g=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.75", "", { "os": "win32", "cpu": "arm64" }, "sha512-j0UB95nmkYGNzmOrs6GqaddO1S90R0YC6IhbKnbKBdjchFPNVLz9JpexAs6MBDXPZwdKAywMxtwG2h3aTJtxng=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.74", "", { "os": "win32", "cpu": "x64" }, "sha512-3wfWXaAKOIlDQz6ZZIESf2M+YGZ7uFHijjTEM8w/STRlLw8Y6+QyGYi1myHSM4d6RSO+/s2EMDxvjDf899W9vQ=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.75", "", { "os": "win32", "cpu": "x64" }, "sha512-ESpVZVGewe3JkB2TwrG3VRbkxT909iPdtvgNT7xTCIYH2VB4jqZomJfvERPTE0tvqAZJm19mHECzJFI8asSJgQ=="],
"@opentui/solid": ["@opentui/solid@0.1.74", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.74", "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-Vz82cI8T9YeJjGsVg4ULp6ral4N+xyt1j9A6Tbu3aaQgEKiB74LW03EXREehfjPr1irOFxtKfWPbx5NKH0Upag=="],
"@opentui/solid": ["@opentui/solid@0.1.75", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.75", "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-WjKsZIfrm29znfRlcD9w3uUn/+uvoy2MmeoDwTvg1YOa0OjCTCmjZ43L9imp0m9S4HmVU8ma6o2bR4COzcyDdg=="],
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
@@ -1747,6 +1749,8 @@
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-EdYd4c9wGvtPB95kqtEyY+bUR+k4kRw3IA30mAQ1jPH6z57AftT8q84qwv0RDp6kkEqOBKxeInKfqi4BESYuqg=="],
"@tauri-apps/plugin-deep-link": ["@tauri-apps/plugin-deep-link@2.4.6", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-UUOSt0U5juK20uhO2MoHZX/IPblkrhUh+VPtIeu3RwtzI0R9Em3Auzfg/PwcZ9Pv8mLne3cQ4p9CFXD6WxqCZA=="],
"@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.4.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-lNIn5CZuw8WZOn8zHzmFmDSzg5zfohWoa3mdULP0YFh/VogVdMVWZPcWSHlydsiJhRQYaTNSYKN7RmZKE2lCYQ=="],
"@tauri-apps/plugin-http": ["@tauri-apps/plugin-http@2.5.4", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-/i4U/9za3mrytTgfRn5RHneKubZE/dwRmshYwyMvNRlkWjvu1m4Ma72kcbVJMZFGXpkbl+qLyWMGrihtWB76Zg=="],
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-olTZ+tKugAY3LxizsJMlbK3TW78HZUoM03PigvQLP4A=",
"aarch64-linux": "sha256-xdKDeqMEnYM2+vGySfb8pbcYyo/xMmgxG/ZhPCKaZEg=",
"aarch64-darwin": "sha256-fihCTrHIiUG+py4vuqdr+YshqSKm2/B5onY50b97sPM=",
"x86_64-darwin": "sha256-inlQQPNAOdkmKK6HQAMI2bG/ZFlfwmUQu9a6vm6Q0jQ="
"x86_64-linux": "sha256-yAtZlh6YR78RwPt0LK/7Pk0qUm0/97+s6ghhZzuoE/0=",
"aarch64-linux": "sha256-6j81rdjQ7Wps9bvfw+mmdwW5p01qUOwX40UZltCTe3Y=",
"aarch64-darwin": "sha256-pDM8M/QMWR6Go5pz3XXsJqcJDHAlHrx2Faijjkzcngo=",
"x86_64-darwin": "sha256-eOAPtMd1n5xYupBOevCLhY1eFy3wzGqFk/EsZocl9Y8="
}
}
+37
View File
@@ -0,0 +1,37 @@
import { test, expect } from "./fixtures"
test.skip("file tree can expand folders and open a file", async ({ page, gotoSession }) => {
await gotoSession()
const toggle = page.getByRole("button", { name: "Toggle file tree" })
const treeTabs = page.locator('[data-component="tabs"][data-variant="pill"][data-scope="filetree"]')
if ((await toggle.getAttribute("aria-expanded")) !== "true") await toggle.click()
await expect(treeTabs).toBeVisible()
await treeTabs.locator('[data-slot="tabs-trigger"]').nth(1).click()
const node = (name: string) => treeTabs.getByRole("button", { name, exact: true })
await expect(node("packages")).toBeVisible()
await node("packages").click()
await expect(node("app")).toBeVisible()
await node("app").click()
await expect(node("src")).toBeVisible()
await node("src").click()
await expect(node("components")).toBeVisible()
await node("components").click()
await expect(node("file-tree.tsx")).toBeVisible()
await node("file-tree.tsx").click()
const tab = page.getByRole("tab", { name: "file-tree.tsx" })
await expect(tab).toBeVisible()
await tab.click()
const code = page.locator('[data-component="code"]').first()
await expect(code.getByText("export default function FileTree")).toBeVisible()
})
+50 -1
View File
@@ -1,5 +1,5 @@
import { test as base, expect } from "@playwright/test"
import { createSdk, dirSlug, getWorktree, promptSelector, sessionPath } from "./utils"
import { createSdk, dirSlug, getWorktree, promptSelector, serverUrl, sessionPath } from "./utils"
type TestFixtures = {
sdk: ReturnType<typeof createSdk>
@@ -29,6 +29,55 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
await use(createSdk(directory))
},
gotoSession: async ({ page, directory }, use) => {
await page.addInitScript(
(input: { directory: string; serverUrl: string }) => {
const key = "opencode.global.dat:server"
const raw = localStorage.getItem(key)
const parsed = (() => {
if (!raw) return undefined
try {
return JSON.parse(raw) as unknown
} catch {
return undefined
}
})()
const store = parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>) : {}
const list = Array.isArray(store.list) ? store.list : []
const lastProject = store.lastProject && typeof store.lastProject === "object" ? store.lastProject : {}
const projects = store.projects && typeof store.projects === "object" ? store.projects : {}
const nextProjects = { ...(projects as Record<string, unknown>) }
const add = (origin: string) => {
const current = nextProjects[origin]
const items = Array.isArray(current) ? current : []
const existing = items.filter(
(p): p is { worktree: string; expanded?: boolean } =>
!!p &&
typeof p === "object" &&
"worktree" in p &&
typeof (p as { worktree?: unknown }).worktree === "string",
)
if (existing.some((p) => p.worktree === input.directory)) return
nextProjects[origin] = [{ worktree: input.directory, expanded: true }, ...existing]
}
add("local")
add(input.serverUrl)
localStorage.setItem(
key,
JSON.stringify({
list,
projects: nextProjects,
lastProject,
}),
)
},
{ directory, serverUrl },
)
const gotoSession = async (sessionID?: string) => {
await page.goto(sessionPath(directory, sessionID))
await expect(page.locator(promptSelector)).toBeVisible()
@@ -0,0 +1,86 @@
import { test, expect } from "./fixtures"
import { modKey, promptSelector } from "./utils"
test("hiding a model removes it from the model picker", async ({ page, gotoSession }) => {
await gotoSession()
await page.locator(promptSelector).click()
await page.keyboard.type("/model")
const command = page.locator('[data-slash-id="model.choose"]')
await expect(command).toBeVisible()
await command.hover()
await page.keyboard.press("Enter")
const picker = page.getByRole("dialog")
await expect(picker).toBeVisible()
const target = picker.locator('[data-slot="list-item"]').first()
await expect(target).toBeVisible()
const key = await target.getAttribute("data-key")
if (!key) throw new Error("Failed to resolve model key from list item")
const name = (await target.locator("span").first().innerText()).trim()
if (!name) throw new Error("Failed to resolve model name from list item")
await page.keyboard.press("Escape")
await expect(picker).toHaveCount(0)
const settings = page.getByRole("dialog")
await page.keyboard.press(`${modKey}+Comma`).catch(() => undefined)
const opened = await settings
.waitFor({ state: "visible", timeout: 3000 })
.then(() => true)
.catch(() => false)
if (!opened) {
await page.getByRole("button", { name: "Settings" }).first().click()
await expect(settings).toBeVisible()
}
await settings.getByRole("tab", { name: "Models" }).click()
const search = settings.getByPlaceholder("Search models")
await expect(search).toBeVisible()
await search.fill(name)
const toggle = settings.locator('[data-component="switch"]').filter({ hasText: name }).first()
const input = toggle.locator('[data-slot="switch-input"]')
await expect(toggle).toBeVisible()
await expect(input).toHaveAttribute("aria-checked", "true")
await toggle.locator('[data-slot="switch-control"]').click()
await expect(input).toHaveAttribute("aria-checked", "false")
await page.keyboard.press("Escape")
const closed = await settings
.waitFor({ state: "detached", timeout: 1500 })
.then(() => true)
.catch(() => false)
if (!closed) {
await page.keyboard.press("Escape")
const closedSecond = await settings
.waitFor({ state: "detached", timeout: 1500 })
.then(() => true)
.catch(() => false)
if (!closedSecond) {
await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } })
await expect(settings).toHaveCount(0)
}
}
await page.locator(promptSelector).click()
await page.keyboard.type("/model")
await expect(command).toBeVisible()
await command.hover()
await page.keyboard.press("Enter")
const pickerAgain = page.getByRole("dialog")
await expect(pickerAgain).toBeVisible()
await expect(pickerAgain.locator('[data-slot="list-item"]').first()).toBeVisible()
await expect(pickerAgain.locator(`[data-slot="list-item"][data-key="${key}"]`)).toHaveCount(0)
await page.keyboard.press("Escape")
await expect(pickerAgain).toHaveCount(0)
})
+67
View File
@@ -0,0 +1,67 @@
import { test, expect } from "./fixtures"
import { serverName, serverUrl } from "./utils"
const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl"
test("can set a default server on web", async ({ page, gotoSession }) => {
await page.addInitScript((key: string) => {
try {
localStorage.removeItem(key)
} catch {
return
}
}, DEFAULT_SERVER_URL_KEY)
await gotoSession()
const status = page.getByRole("button", { name: "Status" })
await expect(status).toBeVisible()
const popover = page.locator('[data-component="popover-content"]').filter({ hasText: "Manage servers" })
const ensurePopoverOpen = async () => {
if (await popover.isVisible()) return
await status.click()
await expect(popover).toBeVisible()
}
await ensurePopoverOpen()
await popover.getByRole("button", { name: "Manage servers" }).click()
const dialog = page.getByRole("dialog")
await expect(dialog).toBeVisible()
const row = dialog.locator('[data-slot="list-item"]').filter({ hasText: serverName }).first()
await expect(row).toBeVisible()
const menu = row.locator('[data-component="icon-button"]').last()
await menu.click()
await page.getByRole("menuitem", { name: "Set as default" }).click()
await expect.poll(() => page.evaluate((key) => localStorage.getItem(key), DEFAULT_SERVER_URL_KEY)).toBe(serverUrl)
await expect(row.getByText("Default", { exact: true })).toBeVisible()
await page.keyboard.press("Escape")
const closed = await dialog
.waitFor({ state: "detached", timeout: 1500 })
.then(() => true)
.catch(() => false)
if (!closed) {
await page.keyboard.press("Escape")
const closedSecond = await dialog
.waitFor({ state: "detached", timeout: 1500 })
.then(() => true)
.catch(() => false)
if (!closedSecond) {
await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } })
await expect(dialog).toHaveCount(0)
}
}
await ensurePopoverOpen()
const serverRow = popover.locator("button").filter({ hasText: serverName }).first()
await expect(serverRow).toBeVisible()
await expect(serverRow.getByText("Default", { exact: true })).toBeVisible()
})
@@ -0,0 +1,56 @@
import { test, expect } from "./fixtures"
import { modKey, promptSelector } from "./utils"
test("smoke providers settings opens provider selector", async ({ page, gotoSession }) => {
await gotoSession()
const dialog = page.getByRole("dialog")
await page.keyboard.press(`${modKey}+Comma`).catch(() => undefined)
const opened = await dialog
.waitFor({ state: "visible", timeout: 3000 })
.then(() => true)
.catch(() => false)
if (!opened) {
await page.getByRole("button", { name: "Settings" }).first().click()
await expect(dialog).toBeVisible()
}
await dialog.getByRole("tab", { name: "Providers" }).click()
await expect(dialog.getByText("Connected providers", { exact: true })).toBeVisible()
await expect(dialog.getByText("Popular providers", { exact: true })).toBeVisible()
await dialog.getByRole("button", { name: "Show more providers" }).click()
const providerDialog = page.getByRole("dialog").filter({ has: page.getByPlaceholder("Search providers") })
await expect(providerDialog).toBeVisible()
await expect(providerDialog.getByPlaceholder("Search providers")).toBeVisible()
await expect(providerDialog.locator('[data-slot="list-item"]').first()).toBeVisible()
await page.keyboard.press("Escape")
await expect(providerDialog).toHaveCount(0)
await expect(page.locator(promptSelector)).toBeVisible()
const stillOpen = await dialog.isVisible().catch(() => false)
if (!stillOpen) return
await page.keyboard.press("Escape")
const closed = await dialog
.waitFor({ state: "detached", timeout: 1500 })
.then(() => true)
.catch(() => false)
if (closed) return
await page.keyboard.press("Escape")
const closedSecond = await dialog
.waitFor({ state: "detached", timeout: 1500 })
.then(() => true)
.catch(() => false)
if (closedSecond) return
await page.locator('[data-component="dialog-overlay"]').click({ position: { x: 5, y: 5 } })
await expect(dialog).toHaveCount(0)
})
@@ -0,0 +1,61 @@
import { test, expect } from "./fixtures"
import { modKey, promptSelector } from "./utils"
type Locator = {
first: () => Locator
getAttribute: (name: string) => Promise<string | null>
scrollIntoViewIfNeeded: () => Promise<void>
click: () => Promise<void>
}
type Page = {
locator: (selector: string) => Locator
keyboard: {
press: (key: string) => Promise<void>
}
}
type Fixtures = {
page: Page
slug: string
sdk: {
session: {
create: (input: { title: string }) => Promise<{ data?: { id?: string } }>
delete: (input: { sessionID: string }) => Promise<unknown>
}
}
gotoSession: (sessionID?: string) => Promise<void>
}
test("sidebar session links navigate to the selected session", async ({ page, slug, sdk, gotoSession }: Fixtures) => {
const stamp = Date.now()
const one = await sdk.session.create({ title: `e2e sidebar nav 1 ${stamp}` }).then((r) => r.data)
const two = await sdk.session.create({ title: `e2e sidebar nav 2 ${stamp}` }).then((r) => r.data)
if (!one?.id) throw new Error("Session create did not return an id")
if (!two?.id) throw new Error("Session create did not return an id")
try {
await gotoSession(one.id)
const main = page.locator("main")
const collapsed = ((await main.getAttribute("class")) ?? "").includes("xl:border-l")
if (collapsed) {
await page.keyboard.press(`${modKey}+B`)
await expect(main).not.toHaveClass(/xl:border-l/)
}
const target = page.locator(`[data-session-id="${two.id}"] a`).first()
await expect(target).toBeVisible()
await target.scrollIntoViewIfNeeded()
await target.click()
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
await expect(page.locator(promptSelector)).toBeVisible()
await expect(page.locator(`[data-session-id="${two.id}"] a`).first()).toHaveClass(/\bactive\b/)
} finally {
await sdk.session.delete({ sessionID: one.id }).catch(() => undefined)
await sdk.session.delete({ sessionID: two.id }).catch(() => undefined)
}
})
+52
View File
@@ -0,0 +1,52 @@
import { test, expect } from "./fixtures"
import { modKey, promptSelector } from "./utils"
test("titlebar back/forward navigates between sessions", async ({ page, slug, sdk, gotoSession }) => {
await page.setViewportSize({ width: 1400, height: 800 })
const stamp = Date.now()
const one = await sdk.session.create({ title: `e2e titlebar history 1 ${stamp}` }).then((r) => r.data)
const two = await sdk.session.create({ title: `e2e titlebar history 2 ${stamp}` }).then((r) => r.data)
if (!one?.id) throw new Error("Session create did not return an id")
if (!two?.id) throw new Error("Session create did not return an id")
try {
await gotoSession(one.id)
const main = page.locator("main")
const collapsed = ((await main.getAttribute("class")) ?? "").includes("xl:border-l")
if (collapsed) {
await page.keyboard.press(`${modKey}+B`)
await expect(main).not.toHaveClass(/xl:border-l/)
}
const link = page.locator(`[data-session-id="${two.id}"] a`).first()
await expect(link).toBeVisible()
await link.scrollIntoViewIfNeeded()
await link.click()
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
await expect(page.locator(promptSelector)).toBeVisible()
const back = page.getByRole("button", { name: "Back" })
const forward = page.getByRole("button", { name: "Forward" })
await expect(back).toBeVisible()
await expect(back).toBeEnabled()
await back.click()
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${one.id}(?:\\?|#|$)`))
await expect(page.locator(promptSelector)).toBeVisible()
await expect(forward).toBeVisible()
await expect(forward).toBeEnabled()
await forward.click()
await expect(page).toHaveURL(new RegExp(`/${slug}/session/${two.id}(?:\\?|#|$)`))
await expect(page.locator(promptSelector)).toBeVisible()
} finally {
await sdk.session.delete({ sessionID: one.id }).catch(() => undefined)
await sdk.session.delete({ sessionID: two.id }).catch(() => undefined)
}
})
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.1.36",
"version": "0.0.0-ci-202601291718",
"description": "",
"type": "module",
"exports": {
+1 -1
View File
@@ -36,7 +36,7 @@ function writeAndWait(term: Terminal, data: string): Promise<void> {
})
}
describe("SerializeAddon", () => {
describe.skip("SerializeAddon", () => {
describe("ANSI color preservation", () => {
test("should preserve text attributes (bold, italic, underline)", async () => {
const { term, addon } = createTerminal()
+22 -7
View File
@@ -14,22 +14,22 @@ import { GlobalSyncProvider } from "@/context/global-sync"
import { PermissionProvider } from "@/context/permission"
import { LayoutProvider } from "@/context/layout"
import { GlobalSDKProvider } from "@/context/global-sdk"
import { ServerProvider, useServer } from "@/context/server"
import { normalizeServerUrl, ServerProvider, useServer } from "@/context/server"
import { SettingsProvider } from "@/context/settings"
import { TerminalProvider } from "@/context/terminal"
import { PromptProvider } from "@/context/prompt"
import { FileProvider } from "@/context/file"
import { CommentsProvider } from "@/context/comments"
import { NotificationProvider } from "@/context/notification"
import { ModelsProvider } from "@/context/models"
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
import { CommandProvider } from "@/context/command"
import { LanguageProvider, useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { Logo } from "@opencode-ai/ui/logo"
import { HighlightsProvider } from "@/context/highlights"
import Layout from "@/pages/layout"
import DirectoryLayout from "@/pages/directory-layout"
import { ErrorPage } from "./pages/error"
import { iife } from "@opencode-ai/util/iife"
import { Suspense } from "solid-js"
const Home = lazy(() => import("@/pages/home"))
@@ -43,7 +43,7 @@ function UiI18nBridge(props: ParentProps) {
declare global {
interface Window {
__OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string }
__OPENCODE__?: { updaterEnabled?: boolean; serverPassword?: string; deepLinks?: string[] }
}
}
@@ -85,8 +85,19 @@ function ServerKey(props: ParentProps) {
}
export function AppInterface(props: { defaultUrl?: string }) {
const platform = usePlatform()
const stored = (() => {
if (platform.platform !== "web") return
const result = platform.getDefaultServerUrl?.()
if (result instanceof Promise) return
if (!result) return
return normalizeServerUrl(result)
})()
const defaultServerUrl = () => {
if (props.defaultUrl) return props.defaultUrl
if (stored) return stored
if (location.hostname.includes("opencode.ai")) return "http://localhost:4096"
if (import.meta.env.DEV)
return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
@@ -105,9 +116,13 @@ export function AppInterface(props: { defaultUrl?: string }) {
<PermissionProvider>
<LayoutProvider>
<NotificationProvider>
<CommandProvider>
<Layout>{props.children}</Layout>
</CommandProvider>
<ModelsProvider>
<CommandProvider>
<HighlightsProvider>
<Layout>{props.children}</Layout>
</HighlightsProvider>
</CommandProvider>
</ModelsProvider>
</NotificationProvider>
</LayoutProvider>
</PermissionProvider>
@@ -27,6 +27,17 @@ export function DialogConnectProvider(props: { provider: string }) {
const globalSDK = useGlobalSDK()
const platform = usePlatform()
const language = useLanguage()
const alive = { value: true }
const timer = { current: undefined as ReturnType<typeof setTimeout> | undefined }
onCleanup(() => {
alive.value = false
if (timer.current === undefined) return
clearTimeout(timer.current)
timer.current = undefined
})
const provider = createMemo(() => globalSync.data.provider.all.find((x) => x.id === props.provider)!)
const methods = createMemo(
() =>
@@ -53,6 +64,11 @@ export function DialogConnectProvider(props: { provider: string }) {
}
async function selectMethod(index: number) {
if (timer.current !== undefined) {
clearTimeout(timer.current)
timer.current = undefined
}
const method = methods()[index]
setStore(
produce((draft) => {
@@ -75,11 +91,15 @@ export function DialogConnectProvider(props: { provider: string }) {
{ throwOnError: true },
)
.then((x) => {
if (!alive.value) return
const elapsed = Date.now() - start
const delay = 1000 - elapsed
if (delay > 0) {
setTimeout(() => {
if (timer.current !== undefined) clearTimeout(timer.current)
timer.current = setTimeout(() => {
timer.current = undefined
if (!alive.value) return
setStore("state", "complete")
setStore("authorization", x.data!)
}, delay)
@@ -89,6 +109,7 @@ export function DialogConnectProvider(props: { provider: string }) {
setStore("authorization", x.data!)
})
.catch((e) => {
if (!alive.value) return
setStore("state", "error")
setStore("error", String(e))
})
@@ -372,26 +393,33 @@ export function DialogConnectProvider(props: { provider: string }) {
return instructions
})
onMount(async () => {
if (store.authorization?.url) {
platform.openLink(store.authorization.url)
}
const result = await globalSDK.client.provider.oauth
.callback({
providerID: props.provider,
method: store.methodIndex,
})
.then((value) =>
value.error ? { ok: false as const, error: value.error } : { ok: true as const },
)
.catch((error) => ({ ok: false as const, error }))
if (!result.ok) {
const message = result.error instanceof Error ? result.error.message : String(result.error)
setStore("state", "error")
setStore("error", message)
return
}
await complete()
onMount(() => {
void (async () => {
if (store.authorization?.url) {
platform.openLink(store.authorization.url)
}
const result = await globalSDK.client.provider.oauth
.callback({
providerID: props.provider,
method: store.methodIndex,
})
.then((value) =>
value.error ? { ok: false as const, error: value.error } : { ok: true as const },
)
.catch((error) => ({ ok: false as const, error }))
if (!alive.value) return
if (!result.ok) {
const message = result.error instanceof Error ? result.error.message : String(result.error)
setStore("state", "error")
setStore("error", message)
return
}
await complete()
})()
})
return (
@@ -0,0 +1,424 @@
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { TextField } from "@opencode-ai/ui/text-field"
import { showToast } from "@opencode-ai/ui/toast"
import { For } from "solid-js"
import { createStore, produce } from "solid-js/store"
import { Link } from "@/components/link"
import { useGlobalSDK } from "@/context/global-sdk"
import { useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language"
import { DialogSelectProvider } from "./dialog-select-provider"
const PROVIDER_ID = /^[a-z0-9][a-z0-9-_]*$/
const OPENAI_COMPATIBLE = "@ai-sdk/openai-compatible"
type Props = {
back?: "providers" | "close"
}
export function DialogCustomProvider(props: Props) {
const dialog = useDialog()
const globalSync = useGlobalSync()
const globalSDK = useGlobalSDK()
const language = useLanguage()
const [form, setForm] = createStore({
providerID: "",
name: "",
baseURL: "",
apiKey: "",
models: [{ id: "", name: "" }],
headers: [{ key: "", value: "" }],
saving: false,
})
const [errors, setErrors] = createStore({
providerID: undefined as string | undefined,
name: undefined as string | undefined,
baseURL: undefined as string | undefined,
models: [{} as { id?: string; name?: string }],
headers: [{} as { key?: string; value?: string }],
})
const goBack = () => {
if (props.back === "close") {
dialog.close()
return
}
dialog.show(() => <DialogSelectProvider />)
}
const addModel = () => {
setForm(
"models",
produce((draft) => {
draft.push({ id: "", name: "" })
}),
)
setErrors(
"models",
produce((draft) => {
draft.push({})
}),
)
}
const removeModel = (index: number) => {
if (form.models.length <= 1) return
setForm(
"models",
produce((draft) => {
draft.splice(index, 1)
}),
)
setErrors(
"models",
produce((draft) => {
draft.splice(index, 1)
}),
)
}
const addHeader = () => {
setForm(
"headers",
produce((draft) => {
draft.push({ key: "", value: "" })
}),
)
setErrors(
"headers",
produce((draft) => {
draft.push({})
}),
)
}
const removeHeader = (index: number) => {
if (form.headers.length <= 1) return
setForm(
"headers",
produce((draft) => {
draft.splice(index, 1)
}),
)
setErrors(
"headers",
produce((draft) => {
draft.splice(index, 1)
}),
)
}
const validate = () => {
const providerID = form.providerID.trim()
const name = form.name.trim()
const baseURL = form.baseURL.trim()
const apiKey = form.apiKey.trim()
const env = apiKey.match(/^\{env:([^}]+)\}$/)?.[1]?.trim()
const key = apiKey && !env ? apiKey : undefined
const idError = !providerID
? "Provider ID is required"
: !PROVIDER_ID.test(providerID)
? "Use lowercase letters, numbers, hyphens, or underscores"
: undefined
const nameError = !name ? "Display name is required" : undefined
const urlError = !baseURL
? "Base URL is required"
: !/^https?:\/\//.test(baseURL)
? "Must start with http:// or https://"
: undefined
const disabled = (globalSync.data.config.disabled_providers ?? []).includes(providerID)
const existingProvider = globalSync.data.provider.all.find((p) => p.id === providerID)
const existsError = idError
? undefined
: existingProvider && !disabled
? "That provider ID already exists"
: undefined
const seenModels = new Set<string>()
const modelErrors = form.models.map((m) => {
const id = m.id.trim()
const modelIdError = !id
? "Required"
: seenModels.has(id)
? "Duplicate"
: (() => {
seenModels.add(id)
return undefined
})()
const modelNameError = !m.name.trim() ? "Required" : undefined
return { id: modelIdError, name: modelNameError }
})
const modelsValid = modelErrors.every((m) => !m.id && !m.name)
const models = Object.fromEntries(form.models.map((m) => [m.id.trim(), { name: m.name.trim() }]))
const seenHeaders = new Set<string>()
const headerErrors = form.headers.map((h) => {
const key = h.key.trim()
const value = h.value.trim()
if (!key && !value) return {}
const keyError = !key
? "Required"
: seenHeaders.has(key.toLowerCase())
? "Duplicate"
: (() => {
seenHeaders.add(key.toLowerCase())
return undefined
})()
const valueError = !value ? "Required" : undefined
return { key: keyError, value: valueError }
})
const headersValid = headerErrors.every((h) => !h.key && !h.value)
const headers = Object.fromEntries(
form.headers
.map((h) => ({ key: h.key.trim(), value: h.value.trim() }))
.filter((h) => !!h.key && !!h.value)
.map((h) => [h.key, h.value]),
)
setErrors(
produce((draft) => {
draft.providerID = idError ?? existsError
draft.name = nameError
draft.baseURL = urlError
draft.models = modelErrors
draft.headers = headerErrors
}),
)
const ok = !idError && !existsError && !nameError && !urlError && modelsValid && headersValid
if (!ok) return
const options = {
baseURL,
...(Object.keys(headers).length ? { headers } : {}),
}
return {
providerID,
name,
key,
config: {
npm: OPENAI_COMPATIBLE,
name,
...(env ? { env: [env] } : {}),
options,
models,
},
}
}
const save = async (e: SubmitEvent) => {
e.preventDefault()
if (form.saving) return
const result = validate()
if (!result) return
setForm("saving", true)
const disabledProviders = globalSync.data.config.disabled_providers ?? []
const nextDisabled = disabledProviders.filter((id) => id !== result.providerID)
const auth = result.key
? globalSDK.client.auth.set({
providerID: result.providerID,
auth: {
type: "api",
key: result.key,
},
})
: Promise.resolve()
auth
.then(() =>
globalSync.updateConfig({ provider: { [result.providerID]: result.config }, disabled_providers: nextDisabled }),
)
.then(() => {
dialog.close()
showToast({
variant: "success",
icon: "circle-check",
title: language.t("provider.connect.toast.connected.title", { provider: result.name }),
description: language.t("provider.connect.toast.connected.description", { provider: result.name }),
})
})
.catch((err: unknown) => {
const message = err instanceof Error ? err.message : String(err)
showToast({ title: language.t("common.requestFailed"), description: message })
})
.finally(() => {
setForm("saving", false)
})
}
return (
<Dialog
title={
<IconButton
tabIndex={-1}
icon="arrow-left"
variant="ghost"
onClick={goBack}
aria-label={language.t("common.goBack")}
/>
}
transition
>
<div class="flex flex-col gap-6 px-2.5 pb-3 overflow-y-auto max-h-[60vh]">
<div class="px-2.5 flex gap-4 items-center">
<ProviderIcon id="synthetic" class="size-5 shrink-0 icon-strong-base" />
<div class="text-16-medium text-text-strong">Custom provider</div>
</div>
<form onSubmit={save} class="px-2.5 pb-6 flex flex-col gap-6">
<p class="text-14-regular text-text-base">
Configure an OpenAI-compatible provider. See the{" "}
<Link href="https://opencode.ai/docs/providers/#custom-provider" tabIndex={-1}>
provider config docs
</Link>
.
</p>
<div class="flex flex-col gap-4">
<TextField
autofocus
label="Provider ID"
placeholder="myprovider"
description="Lowercase letters, numbers, hyphens, or underscores"
value={form.providerID}
onChange={setForm.bind(null, "providerID")}
validationState={errors.providerID ? "invalid" : undefined}
error={errors.providerID}
/>
<TextField
label="Display name"
placeholder="My AI Provider"
value={form.name}
onChange={setForm.bind(null, "name")}
validationState={errors.name ? "invalid" : undefined}
error={errors.name}
/>
<TextField
label="Base URL"
placeholder="https://api.myprovider.com/v1"
value={form.baseURL}
onChange={setForm.bind(null, "baseURL")}
validationState={errors.baseURL ? "invalid" : undefined}
error={errors.baseURL}
/>
<TextField
label="API key"
placeholder="API key"
description="Optional. Leave empty if you manage auth via headers."
value={form.apiKey}
onChange={setForm.bind(null, "apiKey")}
/>
</div>
<div class="flex flex-col gap-3">
<label class="text-12-medium text-text-weak">Models</label>
<For each={form.models}>
{(m, i) => (
<div class="flex gap-2 items-start">
<div class="flex-1">
<TextField
label="ID"
hideLabel
placeholder="model-id"
value={m.id}
onChange={(v) => setForm("models", i(), "id", v)}
validationState={errors.models[i()]?.id ? "invalid" : undefined}
error={errors.models[i()]?.id}
/>
</div>
<div class="flex-1">
<TextField
label="Name"
hideLabel
placeholder="Display Name"
value={m.name}
onChange={(v) => setForm("models", i(), "name", v)}
validationState={errors.models[i()]?.name ? "invalid" : undefined}
error={errors.models[i()]?.name}
/>
</div>
<IconButton
type="button"
icon="trash"
variant="ghost"
class="mt-1.5"
onClick={() => removeModel(i())}
disabled={form.models.length <= 1}
aria-label="Remove model"
/>
</div>
)}
</For>
<Button type="button" size="small" variant="ghost" icon="plus-small" onClick={addModel} class="self-start">
Add model
</Button>
</div>
<div class="flex flex-col gap-3">
<label class="text-12-medium text-text-weak">Headers (optional)</label>
<For each={form.headers}>
{(h, i) => (
<div class="flex gap-2 items-start">
<div class="flex-1">
<TextField
label="Header"
hideLabel
placeholder="Header-Name"
value={h.key}
onChange={(v) => setForm("headers", i(), "key", v)}
validationState={errors.headers[i()]?.key ? "invalid" : undefined}
error={errors.headers[i()]?.key}
/>
</div>
<div class="flex-1">
<TextField
label="Value"
hideLabel
placeholder="value"
value={h.value}
onChange={(v) => setForm("headers", i(), "value", v)}
validationState={errors.headers[i()]?.value ? "invalid" : undefined}
error={errors.headers[i()]?.value}
/>
</div>
<IconButton
type="button"
icon="trash"
variant="ghost"
class="mt-1.5"
onClick={() => removeHeader(i())}
disabled={form.headers.length <= 1}
aria-label="Remove header"
/>
</div>
)}
</For>
<Button type="button" size="small" variant="ghost" icon="plus-small" onClick={addHeader} class="self-start">
Add header
</Button>
</div>
<Button class="w-auto self-start" type="submit" size="large" variant="primary" disabled={form.saving}>
{form.saving ? "Saving..." : language.t("common.submit")}
</Button>
</form>
</div>
</Dialog>
)
}
@@ -3,7 +3,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
import { TextField } from "@opencode-ai/ui/text-field"
import { Icon } from "@opencode-ai/ui/icon"
import { createMemo, createSignal, For, Show } from "solid-js"
import { createMemo, For, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { useGlobalSDK } from "@/context/global-sdk"
import { useGlobalSync } from "@/context/global-sync"
@@ -29,35 +29,34 @@ export function DialogEditProject(props: { project: LocalProject }) {
iconUrl: props.project.icon?.override || "",
startup: props.project.commands?.start ?? "",
saving: false,
dragOver: false,
iconHover: false,
})
const [dragOver, setDragOver] = createSignal(false)
const [iconHover, setIconHover] = createSignal(false)
function handleFileSelect(file: File) {
if (!file.type.startsWith("image/")) return
const reader = new FileReader()
reader.onload = (e) => {
setStore("iconUrl", e.target?.result as string)
setIconHover(false)
setStore("iconHover", false)
}
reader.readAsDataURL(file)
}
function handleDrop(e: DragEvent) {
e.preventDefault()
setDragOver(false)
setStore("dragOver", false)
const file = e.dataTransfer?.files[0]
if (file) handleFileSelect(file)
}
function handleDragOver(e: DragEvent) {
e.preventDefault()
setDragOver(true)
setStore("dragOver", true)
}
function handleDragLeave() {
setDragOver(false)
setStore("dragOver", false)
}
function handleInputChange(e: Event) {
@@ -116,19 +115,23 @@ export function DialogEditProject(props: { project: LocalProject }) {
<div class="flex flex-col gap-2">
<label class="text-12-medium text-text-weak">{language.t("dialog.project.edit.icon")}</label>
<div class="flex gap-3 items-start">
<div class="relative" onMouseEnter={() => setIconHover(true)} onMouseLeave={() => setIconHover(false)}>
<div
class="relative"
onMouseEnter={() => setStore("iconHover", true)}
onMouseLeave={() => setStore("iconHover", false)}
>
<div
class="relative size-16 rounded-md transition-colors cursor-pointer"
classList={{
"border-text-interactive-base bg-surface-info-base/20": dragOver(),
"border-border-base hover:border-border-strong": !dragOver(),
"border-text-interactive-base bg-surface-info-base/20": store.dragOver,
"border-border-base hover:border-border-strong": !store.dragOver,
"overflow-hidden": !!store.iconUrl,
}}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onClick={() => {
if (store.iconUrl && iconHover()) {
if (store.iconUrl && store.iconHover) {
clearIcon()
} else {
document.getElementById("icon-upload")?.click()
@@ -142,8 +145,7 @@ export function DialogEditProject(props: { project: LocalProject }) {
<Avatar
fallback={store.name || defaultName()}
{...getAvatarColors(store.color)}
class="size-full"
style={{ "font-size": "32px" }}
class="size-full text-[32px]"
/>
</div>
}
@@ -156,39 +158,19 @@ export function DialogEditProject(props: { project: LocalProject }) {
</Show>
</div>
<div
style={{
position: "absolute",
top: 0,
left: 0,
width: "64px",
height: "64px",
background: "rgba(0,0,0,0.6)",
"border-radius": "6px",
"z-index": 10,
"pointer-events": "none",
opacity: iconHover() && !store.iconUrl ? 1 : 0,
display: "flex",
"align-items": "center",
"justify-content": "center",
class="absolute inset-0 size-16 bg-black/60 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
classList={{
"opacity-100": store.iconHover && !store.iconUrl,
"opacity-0": !(store.iconHover && !store.iconUrl),
}}
>
<Icon name="cloud-upload" size="large" class="text-icon-invert-base" />
</div>
<div
style={{
position: "absolute",
top: 0,
left: 0,
width: "64px",
height: "64px",
background: "rgba(0,0,0,0.6)",
"border-radius": "6px",
"z-index": 10,
"pointer-events": "none",
opacity: iconHover() && store.iconUrl ? 1 : 0,
display: "flex",
"align-items": "center",
"justify-content": "center",
class="absolute inset-0 size-16 bg-black/60 rounded-[6px] z-10 pointer-events-none flex items-center justify-center transition-opacity"
classList={{
"opacity-100": store.iconHover && !!store.iconUrl,
"opacity-0": !(store.iconHover && !!store.iconUrl),
}}
>
<Icon name="trash" size="large" class="text-icon-invert-base" />
+2 -6
View File
@@ -90,12 +90,8 @@ export const DialogFork: Component = () => {
>
{(item) => (
<div class="w-full flex items-center gap-2">
<span class="truncate flex-1 min-w-0 text-left" style={{ "font-weight": "400" }}>
{item.text}
</span>
<span class="text-text-weak shrink-0" style={{ "font-weight": "400" }}>
{item.time}
</span>
<span class="truncate flex-1 min-w-0 text-left font-normal">{item.text}</span>
<span class="text-text-weak shrink-0 font-normal">{item.time}</span>
</div>
)}
</List>
@@ -1,16 +1,33 @@
import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
import { Switch } from "@opencode-ai/ui/switch"
import { Button } from "@opencode-ai/ui/button"
import type { Component } from "solid-js"
import { useLocal } from "@/context/local"
import { popularProviders } from "@/hooks/use-providers"
import { useLanguage } from "@/context/language"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { DialogSelectProvider } from "./dialog-select-provider"
export const DialogManageModels: Component = () => {
const local = useLocal()
const language = useLanguage()
const dialog = useDialog()
const handleConnectProvider = () => {
dialog.show(() => <DialogSelectProvider />)
}
return (
<Dialog title={language.t("dialog.model.manage")} description={language.t("dialog.model.manage.description")}>
<Dialog
title={language.t("dialog.model.manage")}
description={language.t("dialog.model.manage.description")}
action={
<Button class="h-7 -my-1 text-14-medium" icon="plus-small" tabIndex={-1} onClick={handleConnectProvider}>
{language.t("command.provider.connect")}
</Button>
}
>
<List
search={{ placeholder: language.t("dialog.model.search.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.model.empty")}
@@ -0,0 +1,158 @@
import { createSignal, createEffect, onMount, onCleanup } from "solid-js"
import { Dialog } from "@opencode-ai/ui/dialog"
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useSettings } from "@/context/settings"
export type Highlight = {
title: string
description: string
media?: {
type: "image" | "video"
src: string
alt?: string
}
}
export function DialogReleaseNotes(props: { highlights: Highlight[] }) {
const dialog = useDialog()
const settings = useSettings()
const [index, setIndex] = createSignal(0)
const total = () => props.highlights.length
const last = () => Math.max(0, total() - 1)
const feature = () => props.highlights[index()] ?? props.highlights[last()]
const isFirst = () => index() === 0
const isLast = () => index() >= last()
const paged = () => total() > 1
function handleNext() {
if (isLast()) return
setIndex(index() + 1)
}
function handleClose() {
dialog.close()
}
function handleDisable() {
settings.general.setReleaseNotes(false)
handleClose()
}
let focusTrap: HTMLDivElement | undefined
function handleKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") {
e.preventDefault()
handleClose()
return
}
if (!paged()) return
if (e.key === "ArrowLeft" && !isFirst()) {
e.preventDefault()
setIndex(index() - 1)
}
if (e.key === "ArrowRight" && !isLast()) {
e.preventDefault()
setIndex(index() + 1)
}
}
onMount(() => {
focusTrap?.focus()
document.addEventListener("keydown", handleKeyDown)
onCleanup(() => document.removeEventListener("keydown", handleKeyDown))
})
// Refocus the trap when index changes to ensure escape always works
createEffect(() => {
index() // track index
focusTrap?.focus()
})
return (
<Dialog
size="large"
fit
class="w-[min(calc(100vw-40px),720px)] h-[min(calc(100vh-40px),400px)] -mt-20 min-h-0 overflow-hidden"
>
{/* Hidden element to capture initial focus and handle escape */}
<div ref={focusTrap} tabindex="0" class="absolute opacity-0 pointer-events-none" />
<div class="flex flex-1 min-w-0 min-h-0">
{/* Left side - Text content */}
<div class="flex flex-col flex-1 min-w-0 p-8">
{/* Top section - feature content (fixed position from top) */}
<div class="flex flex-col gap-2 pt-22">
<div class="flex items-center gap-2">
<h1 class="text-16-medium text-text-strong">{feature()?.title ?? ""}</h1>
</div>
<p class="text-14-regular text-text-base">{feature()?.description ?? ""}</p>
</div>
{/* Spacer to push buttons to bottom */}
<div class="flex-1" />
{/* Bottom section - buttons and indicators (fixed position) */}
<div class="flex flex-col gap-12">
<div class="flex flex-col items-start gap-3">
{isLast() ? (
<Button variant="primary" size="large" onClick={handleClose}>
Get started
</Button>
) : (
<Button variant="secondary" size="large" onClick={handleNext}>
Next
</Button>
)}
<Button variant="ghost" size="small" onClick={handleDisable}>
Don't show these in the future
</Button>
</div>
{paged() && (
<div class="flex items-center gap-1.5 -my-2.5">
{props.highlights.map((_, i) => (
<button
type="button"
class="h-6 flex items-center cursor-pointer bg-transparent border-none p-0 transition-all duration-200"
classList={{
"w-8": i === index(),
"w-3": i !== index(),
}}
onClick={() => setIndex(i)}
>
<div
class="w-full h-0.5 rounded-[1px] transition-colors duration-200"
classList={{
"bg-icon-strong-base": i === index(),
"bg-icon-weak-base": i !== index(),
}}
/>
</button>
))}
</div>
)}
</div>
</div>
{/* Right side - Media content (edge to edge) */}
{feature()?.media && (
<div class="flex-1 min-w-0 bg-surface-base overflow-hidden rounded-r-xl">
{feature()!.media!.type === "image" ? (
<img
src={feature()!.media!.src}
alt={feature()!.media!.alt ?? feature()?.title ?? "Release preview"}
class="w-full h-full object-cover"
/>
) : (
<video src={feature()!.media!.src} autoplay loop muted playsinline class="w-full h-full object-cover" />
)}
</div>
)}
</div>
</Dialog>
)
}
@@ -24,16 +24,18 @@ type Entry = {
path?: string
}
export function DialogSelectFile() {
type DialogSelectFileMode = "all" | "files"
export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFile?: (path: string) => void }) {
const command = useCommand()
const language = useLanguage()
const layout = useLayout()
const file = useFile()
const dialog = useDialog()
const params = useParams()
const filesOnly = () => props.mode === "files"
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const tabs = createMemo(() => layout.tabs(sessionKey))
const view = createMemo(() => layout.view(sessionKey))
const state = { cleanup: undefined as (() => void) | void, committed: false }
const [grouped, setGrouped] = createSignal(false)
const common = [
@@ -46,11 +48,12 @@ export function DialogSelectFile() {
]
const limit = 5
const allowed = createMemo(() =>
command.options.filter(
const allowed = createMemo(() => {
if (filesOnly()) return []
return command.options.filter(
(option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open",
),
)
)
})
const commandItem = (option: CommandOption): Entry => ({
id: "command:" + option.id,
@@ -99,10 +102,50 @@ export function DialogSelectFile() {
return items.slice(0, limit)
})
const items = async (filter: string) => {
const query = filter.trim()
const root = createMemo(() => {
const nodes = file.tree.children("")
const paths = nodes
.filter((node) => node.type === "file")
.map((node) => node.path)
.sort((a, b) => a.localeCompare(b))
return paths.slice(0, limit).map(fileItem)
})
const unique = (items: Entry[]) => {
const seen = new Set<string>()
const out: Entry[] = []
for (const item of items) {
if (seen.has(item.id)) continue
seen.add(item.id)
out.push(item)
}
return out
}
const items = async (text: string) => {
const query = text.trim()
setGrouped(query.length > 0)
if (!query && filesOnly()) {
const loaded = file.tree.state("")?.loaded
const pending = loaded ? Promise.resolve() : file.tree.list("")
const next = unique([...recent(), ...root()])
if (loaded || next.length > 0) {
void pending
return next
}
await pending
return unique([...recent(), ...root()])
}
if (!query) return [...picks(), ...recent()]
if (filesOnly()) {
const files = await file.searchFiles(query)
return files.map(fileItem)
}
const files = await file.searchFiles(query)
const entries = files.map(fileItem)
return [...list(), ...entries]
@@ -119,7 +162,9 @@ export function DialogSelectFile() {
const value = file.tab(path)
tabs().open(value)
file.load(path)
view().reviewPanel.open()
layout.fileTree.open()
layout.fileTree.setTab("all")
props.onOpenFile?.(path)
}
const handleSelect = (item: Entry | undefined) => {
@@ -143,13 +188,14 @@ export function DialogSelectFile() {
})
return (
<Dialog class="pt-3 pb-0 !max-h-[480px]">
<Dialog class="pt-3 pb-0 !max-h-[480px]" transition>
<List
search={{
placeholder: language.t("palette.search.placeholder"),
placeholder: filesOnly()
? language.t("session.header.searchFiles")
: language.t("palette.search.placeholder"),
autofocus: true,
hideIcon: true,
class: "pl-3 pr-2 !mb-0",
}}
emptyMessage={language.t("palette.empty")}
loadingMessage={language.t("common.loading")}
@@ -177,7 +223,7 @@ export function DialogSelectFile() {
</div>
}
>
<div class="w-full flex items-center justify-between gap-4 pl-1">
<div class="w-full flex items-center justify-between gap-4">
<div class="flex items-center gap-2 min-w-0">
<span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span>
<Show when={item.description}>
@@ -54,6 +54,7 @@ const ModelList: Component<{
class="w-full"
placement="right-start"
gutter={12}
forceMount={false}
value={
<ModelTooltip
model={item}
@@ -110,6 +111,11 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
setStore("open", false)
dialog.show(() => <DialogManageModels />)
}
const handleConnectProvider = () => {
setStore("open", false)
dialog.show(() => <DialogSelectProvider />)
}
const language = useLanguage()
createEffect(() => {
@@ -181,7 +187,7 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
<Kobalte.Portal>
<Kobalte.Content
ref={(el) => setStore("content", el)}
class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden"
class="w-72 h-80 flex flex-col p-2 rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden"
onEscapeKeyDown={(event) => {
setStore("dismiss", "escape")
setStore("open", false)
@@ -207,15 +213,28 @@ export function ModelSelectorPopover<T extends ValidComponent = "div">(props: {
onSelect={() => setStore("open", false)}
class="p-1"
action={
<IconButton
icon="sliders"
variant="ghost"
iconSize="normal"
class="size-6"
aria-label={language.t("dialog.model.manage")}
title={language.t("dialog.model.manage")}
onClick={handleManage}
/>
<div class="flex items-center gap-1">
<Tooltip placement="top" forceMount={false} value={language.t("command.provider.connect")}>
<IconButton
icon="plus-small"
variant="ghost"
iconSize="normal"
class="size-6"
aria-label={language.t("command.provider.connect")}
onClick={handleConnectProvider}
/>
</Tooltip>
<Tooltip placement="top" forceMount={false} value={language.t("dialog.model.manage")}>
<IconButton
icon="sliders"
variant="ghost"
iconSize="normal"
class="size-6"
aria-label={language.t("dialog.model.manage")}
onClick={handleManage}
/>
</Tooltip>
</div>
}
/>
</Kobalte.Content>
@@ -5,9 +5,17 @@ import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
import { Tag } from "@opencode-ai/ui/tag"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { IconName } from "@opencode-ai/ui/icons/provider"
import { iconNames, type IconName } from "@opencode-ai/ui/icons/provider"
import { DialogConnectProvider } from "./dialog-connect-provider"
import { useLanguage } from "@/context/language"
import { DialogCustomProvider } from "./dialog-custom-provider"
const CUSTOM_ID = "_custom"
function icon(id: string): IconName {
if (iconNames.includes(id as IconName)) return id as IconName
return "synthetic"
}
export const DialogSelectProvider: Component = () => {
const dialog = useDialog()
@@ -18,7 +26,7 @@ export const DialogSelectProvider: Component = () => {
const otherGroup = () => language.t("dialog.provider.group.other")
return (
<Dialog title={language.t("command.provider.connect")}>
<Dialog title={language.t("command.provider.connect")} transition>
<List
search={{ placeholder: language.t("dialog.provider.search.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.provider.empty")}
@@ -26,11 +34,13 @@ export const DialogSelectProvider: Component = () => {
key={(x) => x?.id}
items={() => {
language.locale()
return providers.all()
return [{ id: CUSTOM_ID, name: "Custom provider" }, ...providers.all()]
}}
filterKeys={["id", "name"]}
groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())}
sortBy={(a, b) => {
if (a.id === CUSTOM_ID) return -1
if (b.id === CUSTOM_ID) return 1
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
return a.name.localeCompare(b.name)
@@ -43,13 +53,20 @@ export const DialogSelectProvider: Component = () => {
}}
onSelect={(x) => {
if (!x) return
if (x.id === CUSTOM_ID) {
dialog.show(() => <DialogCustomProvider back="providers" />)
return
}
dialog.show(() => <DialogConnectProvider provider={x.id} />)
}}
>
{(i) => (
<div class="px-1.25 w-full flex items-center gap-x-3">
<ProviderIcon data-slot="list-item-extra-icon" id={i.id as IconName} />
<ProviderIcon data-slot="list-item-extra-icon" id={icon(i.id)} />
<span>{i.name}</span>
<Show when={i.id === CUSTOM_ID}>
<Tag>{language.t("settings.providers.tag.custom")}</Tag>
</Show>
<Show when={i.id === "opencode"}>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</Show>
@@ -14,6 +14,7 @@ import { useLanguage } from "@/context/language"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { useGlobalSDK } from "@/context/global-sdk"
import { showToast } from "@opencode-ai/ui/toast"
type ServerStatus = { healthy: boolean; version?: string }
@@ -40,10 +41,11 @@ interface EditRowProps {
}
async function checkHealth(url: string, platform: ReturnType<typeof usePlatform>): Promise<ServerStatus> {
const signal = (AbortSignal as unknown as { timeout?: (ms: number) => AbortSignal }).timeout?.(3000)
const sdk = createOpencodeClient({
baseUrl: url,
fetch: platform.fetch,
signal: AbortSignal.timeout(3000),
signal,
})
return sdk.global
.health()
@@ -57,18 +59,16 @@ function AddRow(props: AddRowProps) {
<div class="flex-1 min-w-0 [&_[data-slot=input-wrapper]]:relative">
<div
classList={{
"size-1.5 rounded-full absolute left-3 z-10 pointer-events-none": true,
"size-1.5 rounded-full absolute left-3 top-1/2 -translate-y-1/2 z-10 pointer-events-none": true,
"bg-icon-success-base": props.status === true,
"bg-icon-critical-base": props.status === false,
"bg-border-weak-base": props.status === undefined,
}}
style={{ top: "50%", transform: "translateY(-50%)" }}
ref={(el) => {
// Position relative to input-wrapper
requestAnimationFrame(() => {
const wrapper = el.parentElement?.querySelector('[data-slot="input-wrapper"]')
if (wrapper instanceof HTMLElement) {
wrapper.style.position = "relative"
wrapper.appendChild(el)
}
})
@@ -149,13 +149,22 @@ export function DialogSelectServer() {
})
const [defaultUrl, defaultUrlActions] = createResource(
async () => {
const url = await platform.getDefaultServerUrl?.()
if (!url) return null
return normalizeServerUrl(url) ?? null
try {
const url = await platform.getDefaultServerUrl?.()
if (!url) return null
return normalizeServerUrl(url) ?? null
} catch (err) {
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: err instanceof Error ? err.message : String(err),
})
return null
}
},
{ initialValue: null },
)
const isDesktop = platform.platform === "desktop"
const canDefault = createMemo(() => !!platform.getDefaultServerUrl && !!platform.setDefaultServerUrl)
const looksComplete = (value: string) => {
const normalized = normalizeServerUrl(value)
@@ -505,11 +514,19 @@ export function DialogSelectServer() {
>
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<Show when={isDesktop && defaultUrl() !== i}>
<Show when={canDefault() && defaultUrl() !== i}>
<DropdownMenu.Item
onSelect={async () => {
await platform.setDefaultServerUrl?.(i)
defaultUrlActions.mutate(i)
try {
await platform.setDefaultServerUrl?.(i)
defaultUrlActions.mutate(i)
} catch (err) {
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: err instanceof Error ? err.message : String(err),
})
}
}}
>
<DropdownMenu.ItemLabel>
@@ -517,11 +534,19 @@ export function DialogSelectServer() {
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</Show>
<Show when={isDesktop && defaultUrl() === i}>
<Show when={canDefault() && defaultUrl() === i}>
<DropdownMenu.Item
onSelect={async () => {
await platform.setDefaultServerUrl?.(null)
defaultUrlActions.mutate(null)
try {
await platform.setDefaultServerUrl?.(null)
defaultUrlActions.mutate(null)
} catch (err) {
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: err instanceof Error ? err.message : String(err),
})
}
}}
>
<DropdownMenu.ItemLabel>
+38 -68
View File
@@ -6,81 +6,54 @@ import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { SettingsGeneral } from "./settings-general"
import { SettingsKeybinds } from "./settings-keybinds"
import { SettingsPermissions } from "./settings-permissions"
import { SettingsProviders } from "./settings-providers"
import { SettingsModels } from "./settings-models"
import { SettingsAgents } from "./settings-agents"
import { SettingsCommands } from "./settings-commands"
import { SettingsMcp } from "./settings-mcp"
export const DialogSettings: Component = () => {
const language = useLanguage()
const platform = usePlatform()
return (
<Dialog size="x-large">
<Dialog size="x-large" transition>
<Tabs orientation="vertical" variant="settings" defaultValue="general" class="h-full settings-dialog">
<Tabs.List>
<div
style={{
display: "flex",
"flex-direction": "column",
"justify-content": "space-between",
height: "100%",
width: "100%",
}}
>
<div
style={{
display: "flex",
"flex-direction": "column",
gap: "12px",
width: "100%",
"padding-top": "12px",
}}
>
<Tabs.SectionTitle>{language.t("settings.section.desktop")}</Tabs.SectionTitle>
<div style={{ display: "flex", "flex-direction": "column", gap: "6px", width: "100%" }}>
<Tabs.Trigger value="general">
<Icon name="sliders" />
{language.t("settings.tab.general")}
</Tabs.Trigger>
<Tabs.Trigger value="shortcuts">
<Icon name="keyboard" />
{language.t("settings.tab.shortcuts")}
</Tabs.Trigger>
<div class="flex flex-col justify-between h-full w-full">
<div class="flex flex-col gap-3 w-full pt-3">
<div class="flex flex-col gap-3">
<div class="flex flex-col gap-1.5">
<Tabs.SectionTitle>{language.t("settings.section.desktop")}</Tabs.SectionTitle>
<div class="flex flex-col gap-1.5 w-full">
<Tabs.Trigger value="general">
<Icon name="sliders" />
{language.t("settings.tab.general")}
</Tabs.Trigger>
<Tabs.Trigger value="shortcuts">
<Icon name="keyboard" />
{language.t("settings.tab.shortcuts")}
</Tabs.Trigger>
</div>
</div>
<div class="flex flex-col gap-1.5">
<Tabs.SectionTitle>{language.t("settings.section.server")}</Tabs.SectionTitle>
<div class="flex flex-col gap-1.5 w-full">
<Tabs.Trigger value="providers">
<Icon name="providers" />
{language.t("settings.providers.title")}
</Tabs.Trigger>
<Tabs.Trigger value="models">
<Icon name="models" />
{language.t("settings.models.title")}
</Tabs.Trigger>
</div>
</div>
</div>
</div>
<div class="flex flex-col gap-1 pl-1 py-1 text-12-medium text-text-weak">
<span>OpenCode Desktop</span>
<span>{language.t("app.name.desktop")}</span>
<span class="text-11-regular">v{platform.version}</span>
</div>
</div>
{/* <Tabs.SectionTitle>Server</Tabs.SectionTitle> */}
{/* <Tabs.Trigger value="permissions"> */}
{/* <Icon name="checklist" /> */}
{/* Permissions */}
{/* </Tabs.Trigger> */}
{/* <Tabs.Trigger value="providers"> */}
{/* <Icon name="server" /> */}
{/* Providers */}
{/* </Tabs.Trigger> */}
{/* <Tabs.Trigger value="models"> */}
{/* <Icon name="brain" /> */}
{/* Models */}
{/* </Tabs.Trigger> */}
{/* <Tabs.Trigger value="agents"> */}
{/* <Icon name="task" /> */}
{/* Agents */}
{/* </Tabs.Trigger> */}
{/* <Tabs.Trigger value="commands"> */}
{/* <Icon name="console" /> */}
{/* Commands */}
{/* </Tabs.Trigger> */}
{/* <Tabs.Trigger value="mcp"> */}
{/* <Icon name="mcp" /> */}
{/* MCP */}
{/* </Tabs.Trigger> */}
</Tabs.List>
<Tabs.Content value="general" class="no-scrollbar">
<SettingsGeneral />
@@ -88,15 +61,12 @@ export const DialogSettings: Component = () => {
<Tabs.Content value="shortcuts" class="no-scrollbar">
<SettingsKeybinds />
</Tabs.Content>
{/* <Tabs.Content value="permissions" class="no-scrollbar"> */}
{/* <SettingsPermissions /> */}
{/* </Tabs.Content> */}
{/* <Tabs.Content value="providers" class="no-scrollbar"> */}
{/* <SettingsProviders /> */}
{/* </Tabs.Content> */}
{/* <Tabs.Content value="models" class="no-scrollbar"> */}
{/* <SettingsModels /> */}
{/* </Tabs.Content> */}
<Tabs.Content value="providers" class="no-scrollbar">
<SettingsProviders />
</Tabs.Content>
<Tabs.Content value="models" class="no-scrollbar">
<SettingsModels />
</Tabs.Content>
{/* <Tabs.Content value="agents" class="no-scrollbar"> */}
{/* <SettingsAgents /> */}
{/* </Tabs.Content> */}
+334 -72
View File
@@ -1,111 +1,373 @@
import { useLocal, type LocalFile } from "@/context/local"
import { useFile } from "@/context/file"
import { Collapsible } from "@opencode-ai/ui/collapsible"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { Icon } from "@opencode-ai/ui/icon"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { For, Match, Switch, type ComponentProps, type ParentProps } from "solid-js"
import {
createEffect,
createMemo,
For,
Match,
Show,
splitProps,
Switch,
untrack,
type ComponentProps,
type ParentProps,
} from "solid-js"
import { Dynamic } from "solid-js/web"
import type { FileNode } from "@opencode-ai/sdk/v2"
type Kind = "add" | "del" | "mix"
type Filter = {
files: Set<string>
dirs: Set<string>
}
export default function FileTree(props: {
path: string
class?: string
nodeClass?: string
active?: string
level?: number
onFileClick?: (file: LocalFile) => void
allowed?: readonly string[]
modified?: readonly string[]
kinds?: ReadonlyMap<string, Kind>
draggable?: boolean
tooltip?: boolean
onFileClick?: (file: FileNode) => void
_filter?: Filter
_marks?: Set<string>
_deeps?: Map<string, number>
_kinds?: ReadonlyMap<string, Kind>
}) {
const local = useLocal()
const file = useFile()
const level = props.level ?? 0
const draggable = () => props.draggable ?? true
const tooltip = () => props.tooltip ?? true
const Node = (p: ParentProps & ComponentProps<"div"> & { node: LocalFile; as?: "div" | "button" }) => (
<Dynamic
component={p.as ?? "div"}
classList={{
"p-0.5 w-full flex items-center gap-x-2 hover:bg-background-element": true,
// "bg-background-element": local.file.active()?.path === p.node.path,
[props.nodeClass ?? ""]: !!props.nodeClass,
}}
style={`padding-left: ${level * 10}px`}
draggable={true}
onDragStart={(e: any) => {
const evt = e as globalThis.DragEvent
evt.dataTransfer!.effectAllowed = "copy"
evt.dataTransfer!.setData("text/plain", `file:${p.node.path}`)
const filter = createMemo(() => {
if (props._filter) return props._filter
// Create custom drag image without margins
const dragImage = document.createElement("div")
dragImage.className =
"flex items-center gap-x-2 px-2 py-1 bg-background-element rounded-md border border-border-1"
dragImage.style.position = "absolute"
dragImage.style.top = "-1000px"
const allowed = props.allowed
if (!allowed) return
// Copy only the icon and text content without padding
const icon = e.currentTarget.querySelector("svg")
const text = e.currentTarget.querySelector("span")
if (icon && text) {
dragImage.innerHTML = icon.outerHTML + text.outerHTML
}
const files = new Set(allowed)
const dirs = new Set<string>()
document.body.appendChild(dragImage)
evt.dataTransfer!.setDragImage(dragImage, 0, 12)
setTimeout(() => document.body.removeChild(dragImage), 0)
}}
{...p}
>
{p.children}
<span
for (const item of allowed) {
const parts = item.split("/")
const parents = parts.slice(0, -1)
for (const [idx] of parents.entries()) {
const dir = parents.slice(0, idx + 1).join("/")
if (dir) dirs.add(dir)
}
}
return { files, dirs }
})
const marks = createMemo(() => {
if (props._marks) return props._marks
const out = new Set<string>()
for (const item of props.modified ?? []) out.add(item)
for (const item of props.kinds?.keys() ?? []) out.add(item)
if (out.size === 0) return
return out
})
const kinds = createMemo(() => {
if (props._kinds) return props._kinds
return props.kinds
})
const deeps = createMemo(() => {
if (props._deeps) return props._deeps
const out = new Map<string, number>()
const visit = (dir: string, lvl: number): number => {
const expanded = file.tree.state(dir)?.expanded ?? false
if (!expanded) return -1
const nodes = file.tree.children(dir)
const max = nodes.reduce((max, node) => {
if (node.type !== "directory") return max
const open = file.tree.state(node.path)?.expanded ?? false
if (!open) return max
return Math.max(max, visit(node.path, lvl + 1))
}, lvl)
out.set(dir, max)
return max
}
visit(props.path, level - 1)
return out
})
createEffect(() => {
const current = filter()
if (!current) return
if (level !== 0) return
for (const dir of current.dirs) {
const expanded = untrack(() => file.tree.state(dir)?.expanded) ?? false
if (expanded) continue
file.tree.expand(dir)
}
})
createEffect(() => {
const path = props.path
untrack(() => void file.tree.list(path))
})
const nodes = createMemo(() => {
const nodes = file.tree.children(props.path)
const current = filter()
if (!current) return nodes
return nodes.filter((node) => {
if (node.type === "file") return current.files.has(node.path)
return current.dirs.has(node.path)
})
})
const Node = (
p: ParentProps &
ComponentProps<"div"> &
ComponentProps<"button"> & {
node: FileNode
as?: "div" | "button"
},
) => {
const [local, rest] = splitProps(p, ["node", "as", "children", "class", "classList"])
return (
<Dynamic
component={local.as ?? "div"}
classList={{
"text-xs whitespace-nowrap truncate": true,
"text-text-muted/40": p.node.ignored,
"text-text-muted/80": !p.node.ignored,
// "!text-text": local.file.active()?.path === p.node.path,
// "!text-primary": local.file.changed(p.node.path),
"w-full min-w-0 h-6 flex items-center justify-start gap-x-1.5 rounded-md px-1.5 py-0 text-left hover:bg-surface-raised-base-hover active:bg-surface-base-active transition-colors cursor-pointer": true,
"bg-surface-base-active": local.node.path === props.active,
...(local.classList ?? {}),
[local.class ?? ""]: !!local.class,
[props.nodeClass ?? ""]: !!props.nodeClass,
}}
style={`padding-left: ${Math.max(0, 8 + level * 12 - (local.node.type === "file" ? 24 : 4))}px`}
draggable={draggable()}
onDragStart={(e: DragEvent) => {
if (!draggable()) return
e.dataTransfer?.setData("text/plain", `file:${local.node.path}`)
e.dataTransfer?.setData("text/uri-list", `file://${local.node.path}`)
if (e.dataTransfer) e.dataTransfer.effectAllowed = "copy"
const dragImage = document.createElement("div")
dragImage.className =
"flex items-center gap-x-2 px-2 py-1 bg-surface-raised-base rounded-md border border-border-base text-12-regular text-text-strong"
dragImage.style.position = "absolute"
dragImage.style.top = "-1000px"
const icon =
(e.currentTarget as HTMLElement).querySelector('[data-component="file-icon"]') ??
(e.currentTarget as HTMLElement).querySelector("svg")
const text = (e.currentTarget as HTMLElement).querySelector("span")
if (icon && text) {
dragImage.innerHTML = (icon as SVGElement).outerHTML + (text as HTMLSpanElement).outerHTML
}
document.body.appendChild(dragImage)
e.dataTransfer?.setDragImage(dragImage, 0, 12)
setTimeout(() => document.body.removeChild(dragImage), 0)
}}
{...rest}
>
{p.node.name}
</span>
{/* <Show when={local.file.changed(p.node.path)}> */}
{/* <span class="ml-auto mr-1 w-1.5 h-1.5 rounded-full bg-primary/50 shrink-0" /> */}
{/* </Show> */}
</Dynamic>
)
{local.children}
{(() => {
const kind = kinds()?.get(local.node.path)
const marked = marks()?.has(local.node.path) ?? false
const active = !!kind && marked && !local.node.ignored
const color =
kind === "add"
? "color: var(--icon-diff-add-base)"
: kind === "del"
? "color: var(--icon-diff-delete-base)"
: kind === "mix"
? "color: var(--icon-diff-modified-base)"
: undefined
return (
<span
classList={{
"flex-1 min-w-0 text-12-medium whitespace-nowrap truncate": true,
"text-text-weaker": local.node.ignored,
"text-text-weak": !local.node.ignored && !active,
}}
style={active ? color : undefined}
>
{local.node.name}
</span>
)
})()}
{(() => {
const kind = kinds()?.get(local.node.path)
if (!kind) return null
if (!marks()?.has(local.node.path)) return null
if (local.node.type === "file") {
const text = kind === "add" ? "A" : kind === "del" ? "D" : "M"
const color =
kind === "add"
? "color: var(--icon-diff-add-base)"
: kind === "del"
? "color: var(--icon-diff-delete-base)"
: "color: var(--icon-diff-modified-base)"
return (
<span class="shrink-0 w-4 text-center text-12-medium" style={color}>
{text}
</span>
)
}
if (local.node.type === "directory") {
const color =
kind === "add"
? "background-color: var(--icon-diff-add-base)"
: kind === "del"
? "background-color: var(--icon-diff-delete-base)"
: "background-color: var(--icon-diff-modified-base)"
return <div class="shrink-0 size-1.5 mr-1.5 rounded-full" style={color} />
}
return null
})()}
</Dynamic>
)
}
return (
<div class={`flex flex-col ${props.class}`}>
<For each={local.file.children(props.path)}>
{(node) => (
<Tooltip forceMount={false} openDelay={2000} value={node.path} placement="right">
<div class={`flex flex-col gap-0.5 ${props.class ?? ""}`}>
<For each={nodes()}>
{(node) => {
const expanded = () => file.tree.state(node.path)?.expanded ?? false
const deep = () => deeps().get(node.path) ?? -1
const Wrapper = (p: ParentProps) => {
if (!tooltip()) return p.children
const parts = node.path.split("/")
const leaf = parts[parts.length - 1] ?? node.path
const head = parts.slice(0, -1).join("/")
const prefix = head ? `${head}/` : ""
const kind = () => kinds()?.get(node.path)
const label = () => {
const k = kind()
if (!k) return
if (k === "add") return "Additions"
if (k === "del") return "Deletions"
return "Modifications"
}
const ignored = () => node.type === "directory" && node.ignored
return (
<Tooltip
forceMount={false}
openDelay={2000}
placement="bottom-start"
class="w-full"
contentStyle={{ "max-width": "480px", width: "fit-content" }}
value={
<div class="flex items-center min-w-0 whitespace-nowrap text-12-regular">
<span
class="min-w-0 truncate text-text-invert-base"
style={{ direction: "rtl", "unicode-bidi": "plaintext" }}
>
{prefix}
</span>
<span class="shrink-0 text-text-invert-strong">{leaf}</span>
<Show when={label()}>
{(t: () => string) => (
<>
<span class="mx-1 font-bold text-text-invert-strong"></span>
<span class="shrink-0 text-text-invert-strong">{t()}</span>
</>
)}
</Show>
<Show when={ignored()}>
<>
<span class="mx-1 font-bold text-text-invert-strong"></span>
<span class="shrink-0 text-text-invert-strong">Ignored</span>
</>
</Show>
</div>
}
>
{p.children}
</Tooltip>
)
}
return (
<Switch>
<Match when={node.type === "directory"}>
<Collapsible
variant="ghost"
class="w-full"
data-scope="filetree"
forceMount={false}
// open={local.file.node(node.path)?.expanded}
onOpenChange={(open) => (open ? local.file.expand(node.path) : local.file.collapse(node.path))}
open={expanded()}
onOpenChange={(open) => (open ? file.tree.expand(node.path) : file.tree.collapse(node.path))}
>
<Collapsible.Trigger>
<Node node={node}>
<Collapsible.Arrow class="text-text-muted/60 ml-1" />
<FileIcon
node={node}
// expanded={local.file.node(node.path).expanded}
class="text-text-muted/60 -ml-1"
/>
</Node>
<Wrapper>
<Node node={node}>
<div class="size-4 flex items-center justify-center text-icon-weak">
<Icon name={expanded() ? "chevron-down" : "chevron-right"} size="small" />
</div>
</Node>
</Wrapper>
</Collapsible.Trigger>
<Collapsible.Content>
<FileTree path={node.path} level={level + 1} onFileClick={props.onFileClick} />
<Collapsible.Content class="relative pt-0.5">
<div
classList={{
"absolute top-0 bottom-0 w-px pointer-events-none bg-border-weak-base opacity-0 transition-opacity duration-150 ease-out motion-reduce:transition-none": true,
"group-hover/filetree:opacity-100": expanded() && deep() === level,
"group-hover/filetree:opacity-50": !(expanded() && deep() === level),
}}
style={`left: ${Math.max(0, 8 + level * 12 - 4) + 8}px`}
/>
<FileTree
path={node.path}
level={level + 1}
allowed={props.allowed}
modified={props.modified}
kinds={props.kinds}
active={props.active}
draggable={props.draggable}
tooltip={props.tooltip}
onFileClick={props.onFileClick}
_filter={filter()}
_marks={marks()}
_deeps={deeps()}
_kinds={kinds()}
/>
</Collapsible.Content>
</Collapsible>
</Match>
<Match when={node.type === "file"}>
<Node node={node} as="button" onClick={() => props.onFileClick?.(node)}>
<div class="w-4 shrink-0" />
<FileIcon node={node} class="text-primary" />
</Node>
<Wrapper>
<Node node={node} as="button" type="button" onClick={() => props.onFileClick?.(node)}>
<div class="w-4 shrink-0" />
<FileIcon node={node} class="text-icon-weak size-4" />
</Node>
</Wrapper>
</Match>
</Switch>
</Tooltip>
)}
)
}}
</For>
</div>
)
+24 -28
View File
@@ -171,7 +171,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const tabs = createMemo(() => layout.tabs(sessionKey))
const view = createMemo(() => layout.view(sessionKey))
const commentInReview = (path: string) => {
const sessionID = params.id
@@ -187,20 +186,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const focus = { file: item.path, id: item.commentID }
comments.setActive(focus)
view().reviewPanel.open()
if (item.commentOrigin === "review") {
tabs().open("review")
requestAnimationFrame(() => comments.setFocus(focus))
return
}
if (item.commentOrigin !== "file" && commentInReview(item.path)) {
tabs().open("review")
const wantsReview = item.commentOrigin === "review" || (item.commentOrigin !== "file" && commentInReview(item.path))
if (wantsReview) {
layout.fileTree.open()
layout.fileTree.setTab("changes")
requestAnimationFrame(() => comments.setFocus(focus))
return
}
layout.fileTree.open()
layout.fileTree.setTab("all")
const tab = files.tab(item.path)
tabs().open(tab)
files.load(item.path)
@@ -1042,13 +1038,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
return
}
const ctrl = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey
if (store.popover) {
if (event.key === "Tab") {
selectPopoverActive()
event.preventDefault()
return
}
if (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter") {
const nav = event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter"
const ctrlNav = ctrl && (event.key === "n" || event.key === "p")
if (nav || ctrlNav) {
if (store.popover === "at") {
atOnKeyDown(event)
event.preventDefault()
@@ -1062,8 +1062,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
}
const ctrl = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey
if (ctrl && event.code === "KeyG") {
if (store.popover) {
setStore("popover", null)
@@ -1563,13 +1561,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
})
const timeoutMs = 5 * 60 * 1000
const timer = { id: undefined as number | undefined }
const timeout = new Promise<Awaited<ReturnType<typeof WorktreeState.wait>>>((resolve) => {
setTimeout(() => {
resolve({ status: "failed", message: "Workspace is still preparing" })
timer.id = window.setTimeout(() => {
resolve({ status: "failed", message: language.t("workspace.error.stillPreparing") })
}, timeoutMs)
})
const result = await Promise.race([WorktreeState.wait(sessionDirectory), abort, timeout])
const result = await Promise.race([WorktreeState.wait(sessionDirectory), abort, timeout]).finally(() => {
if (timer.id === undefined) return
clearTimeout(timer.id)
})
pending.delete(session.id)
if (controller.signal.aborted) return false
if (result.status === "failed") throw new Error(result.message)
@@ -1746,10 +1748,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<Tooltip
value={
<span class="flex max-w-[300px]">
<span
class="text-text-invert-base truncate min-w-0"
style={{ direction: "rtl", "text-align": "left", "unicode-bidi": "plaintext" }}
>
<span class="text-text-invert-base truncate-start [unicode-bidi:plaintext] min-w-0">
{getDirectory(item.path)}
</span>
<span class="shrink-0">{getFilename(item.path)}</span>
@@ -1772,10 +1771,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
>
<div class="flex items-center gap-1.5">
<FileIcon node={{ path: item.path, type: "file" }} class="shrink-0 size-3.5" />
<div
class="flex items-center text-11-regular min-w-0"
style={{ "font-weight": "var(--font-weight-medium)" }}
>
<div class="flex items-center text-11-regular min-w-0 font-medium">
<span class="text-text-strong whitespace-nowrap">{getFilenameTruncated(item.path, 14)}</span>
<Show when={item.selection}>
{(sel) => (
@@ -1863,9 +1859,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
store.mode === "shell"
? language.t("prompt.placeholder.shell")
: commentCount() > 1
? "Summarize comments"
? language.t("prompt.placeholder.summarizeComments")
: commentCount() === 1
? "Summarize comment"
? language.t("prompt.placeholder.summarizeComment")
: language.t("prompt.placeholder.normal", { example: language.t(EXAMPLES[store.placeholder]) })
}
contenteditable="true"
@@ -1887,9 +1883,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
{store.mode === "shell"
? language.t("prompt.placeholder.shell")
: commentCount() > 1
? "Summarize comments"
? language.t("prompt.placeholder.summarizeComments")
: commentCount() === 1
? "Summarize comment"
? language.t("prompt.placeholder.summarizeComment")
: language.t("prompt.placeholder.normal", { example: language.t(EXAMPLES[store.placeholder]) })}
</div>
</Show>
@@ -23,7 +23,6 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
const variant = createMemo(() => props.variant ?? "button")
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const tabs = createMemo(() => layout.tabs(sessionKey))
const view = createMemo(() => layout.view(sessionKey))
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
const usd = createMemo(
@@ -58,7 +57,8 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
const openContext = () => {
if (!params.id) return
view().reviewPanel.open()
layout.fileTree.open()
layout.fileTree.setTab("all")
tabs().open("context")
tabs().setActive("context")
}
@@ -9,7 +9,7 @@ import { usePlatform } from "@/context/platform"
import { useSync } from "@/context/sync"
import { useGlobalSDK } from "@/context/global-sdk"
import { getFilename } from "@opencode-ai/util/path"
import { base64Decode } from "@opencode-ai/util/encode"
import { decode64 } from "@/utils/base64"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
@@ -29,7 +29,7 @@ export function SessionHeader() {
const platform = usePlatform()
const language = useLanguage()
const projectDirectory = createMemo(() => base64Decode(params.dir ?? ""))
const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
const project = createMemo(() => {
const directory = projectDirectory()
if (!directory) return
@@ -45,7 +45,6 @@ export function SessionHeader() {
const currentSession = createMemo(() => sync.data.session.find((s) => s.id === params.id))
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
const showShare = createMemo(() => shareEnabled() && !!currentSession())
const showReview = createMemo(() => !!currentSession())
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const view = createMemo(() => layout.view(sessionKey))
@@ -131,7 +130,7 @@ export function SessionHeader() {
<Portal mount={mount()}>
<button
type="button"
class="hidden md:flex w-[320px] p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
class="hidden md:flex w-[320px] p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus-visible:bg-surface-raised-base-hover active:bg-surface-raised-base-active"
onClick={() => command.trigger("file.open")}
aria-label={language.t("session.header.searchFiles")}
>
@@ -284,28 +283,27 @@ export function SessionHeader() {
<TooltipKeybind title={language.t("command.review.toggle")} keybind={command.keybind("review.toggle")}>
<Button
variant="ghost"
class="group/review-toggle size-6 p-0"
onClick={() => view().reviewPanel.toggle()}
class="group/file-tree-toggle size-6 p-0"
onClick={() => layout.fileTree.toggle()}
aria-label={language.t("command.review.toggle")}
aria-expanded={view().reviewPanel.opened()}
aria-expanded={layout.fileTree.opened()}
aria-controls="review-panel"
tabIndex={showReview() ? 0 : -1}
>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
size="small"
name={view().reviewPanel.opened() ? "layout-right-full" : "layout-right"}
class="group-hover/review-toggle:hidden"
name={layout.fileTree.opened() ? "layout-right-full" : "layout-right"}
class="group-hover/file-tree-toggle:hidden"
/>
<Icon
size="small"
name="layout-right-partial"
class="hidden group-hover/review-toggle:inline-block"
class="hidden group-hover/file-tree-toggle:inline-block"
/>
<Icon
size="small"
name={view().reviewPanel.opened() ? "layout-right" : "layout-right-full"}
class="hidden group-active/review-toggle:inline-block"
name={layout.fileTree.opened() ? "layout-right" : "layout-right-full"}
class="hidden group-active/file-tree-toggle:inline-block"
/>
</div>
</Button>
@@ -45,10 +45,7 @@ export function NewSessionView(props: NewSessionViewProps) {
}
return (
<div
class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6"
style={{ "padding-bottom": "calc(var(--prompt-height, 11.25rem) + 64px)" }}
>
<div class="size-full flex flex-col justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6 pb-[calc(var(--prompt-height,11.25rem)+64px)]">
<div class="text-20-medium text-text-weaker">{language.t("command.session.new")}</div>
<div class="flex justify-center items-center gap-3">
<Icon name="folder" size="small" />
@@ -11,7 +11,7 @@ import { useLanguage } from "@/context/language"
export function FileVisual(props: { path: string; active?: boolean }): JSX.Element {
return (
<div class="flex items-center gap-x-1.5">
<div class="flex items-center gap-x-1.5 min-w-0">
<FileIcon
node={{ path: props.path, type: "file" }}
classList={{
@@ -19,7 +19,7 @@ export function FileVisual(props: { path: string; active?: boolean }): JSX.Eleme
"grayscale-0": props.active,
}}
/>
<span class="text-14-medium">{getFilename(props.path)}</span>
<span class="text-14-medium truncate">{getFilename(props.path)}</span>
</div>
)
}
@@ -38,8 +38,9 @@ export function SortableTab(props: { tab: string; onTabClose: (tab: string) => v
closeButton={
<Tooltip value={language.t("common.closeTab")} placement="bottom">
<IconButton
icon="close"
icon="close-small"
variant="ghost"
class="h-5 w-5"
onClick={() => props.onTabClose(props.tab)}
aria-label={language.t("common.closeTab")}
/>
@@ -1,5 +1,6 @@
import type { JSX } from "solid-js"
import { createSignal, Show } from "solid-js"
import { Show } from "solid-js"
import { createStore } from "solid-js/store"
import { createSortable } from "@thisbeyond/solid-dnd"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tabs } from "@opencode-ai/ui/tabs"
@@ -12,11 +13,13 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
const terminal = useTerminal()
const language = useLanguage()
const sortable = createSortable(props.terminal.id)
const [editing, setEditing] = createSignal(false)
const [title, setTitle] = createSignal(props.terminal.title)
const [menuOpen, setMenuOpen] = createSignal(false)
const [menuPosition, setMenuPosition] = createSignal({ x: 0, y: 0 })
const [blurEnabled, setBlurEnabled] = createSignal(false)
const [store, setStore] = createStore({
editing: false,
title: props.terminal.title,
menuOpen: false,
menuPosition: { x: 0, y: 0 },
blurEnabled: false,
})
const isDefaultTitle = () => {
const number = props.terminal.titleNumber
@@ -47,7 +50,7 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
}
const focus = () => {
if (editing()) return
if (store.editing) return
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur()
@@ -71,26 +74,26 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
e.preventDefault()
}
setBlurEnabled(false)
setTitle(props.terminal.title)
setEditing(true)
setStore("blurEnabled", false)
setStore("title", props.terminal.title)
setStore("editing", true)
setTimeout(() => {
const input = document.getElementById(`terminal-title-input-${props.terminal.id}`) as HTMLInputElement
if (!input) return
input.focus()
input.select()
setTimeout(() => setBlurEnabled(true), 100)
setTimeout(() => setStore("blurEnabled", true), 100)
}, 10)
}
const save = () => {
if (!blurEnabled()) return
if (!store.blurEnabled) return
const value = title().trim()
const value = store.title.trim()
if (value && value !== props.terminal.title) {
terminal.update({ id: props.terminal.id, title: value })
}
setEditing(false)
setStore("editing", false)
}
const keydown = (e: KeyboardEvent) => {
@@ -101,14 +104,14 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
}
if (e.key === "Escape") {
e.preventDefault()
setEditing(false)
setStore("editing", false)
}
}
const menu = (e: MouseEvent) => {
e.preventDefault()
setMenuPosition({ x: e.clientX, y: e.clientY })
setMenuOpen(true)
setStore("menuPosition", { x: e.clientX, y: e.clientY })
setStore("menuOpen", true)
}
return (
@@ -143,17 +146,17 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
/>
}
>
<span onDblClick={edit} style={{ visibility: editing() ? "hidden" : "visible" }}>
<span onDblClick={edit} classList={{ invisible: store.editing }}>
{label()}
</span>
</Tabs.Trigger>
<Show when={editing()}>
<Show when={store.editing}>
<div class="absolute inset-0 flex items-center px-3 bg-muted z-10 pointer-events-auto">
<input
id={`terminal-title-input-${props.terminal.id}`}
type="text"
value={title()}
onInput={(e) => setTitle(e.currentTarget.value)}
value={store.title}
onInput={(e) => setStore("title", e.currentTarget.value)}
onBlur={save}
onKeyDown={keydown}
onMouseDown={(e) => e.stopPropagation()}
@@ -161,13 +164,13 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () =>
/>
</div>
</Show>
<DropdownMenu open={menuOpen()} onOpenChange={setMenuOpen}>
<DropdownMenu open={store.menuOpen} onOpenChange={(open) => setStore("menuOpen", open)}>
<DropdownMenu.Portal>
<DropdownMenu.Content
class="fixed"
style={{
position: "fixed",
left: `${menuPosition().x}px`,
top: `${menuPosition().y}px`,
left: `${store.menuPosition.x}px`,
top: `${store.menuPosition.y}px`,
}}
>
<DropdownMenu.Item onSelect={edit}>
+111 -10
View File
@@ -1,8 +1,12 @@
import { Component, createMemo, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import { Button } from "@opencode-ai/ui/button"
import { Select } from "@opencode-ai/ui/select"
import { Switch } from "@opencode-ai/ui/switch"
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
import { showToast } from "@opencode-ai/ui/toast"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { useSettings, monoFontFamily } from "@/context/settings"
import { playSound, SOUND_OPTIONS } from "@/utils/sound"
import { Link } from "./link"
@@ -29,8 +33,67 @@ const playDemoSound = (src: string) => {
export const SettingsGeneral: Component = () => {
const theme = useTheme()
const language = useLanguage()
const platform = usePlatform()
const settings = useSettings()
const [store, setStore] = createStore({
checking: false,
})
const check = () => {
if (!platform.checkUpdate) return
setStore("checking", true)
void platform
.checkUpdate()
.then((result) => {
if (!result.updateAvailable) {
showToast({
variant: "success",
icon: "circle-check",
title: language.t("settings.updates.toast.latest.title"),
description: language.t("settings.updates.toast.latest.description", { version: platform.version ?? "" }),
})
return
}
const actions =
platform.update && platform.restart
? [
{
label: language.t("toast.update.action.installRestart"),
onClick: async () => {
await platform.update!()
await platform.restart!()
},
},
{
label: language.t("toast.update.action.notYet"),
onClick: "dismiss" as const,
},
]
: [
{
label: language.t("toast.update.action.notYet"),
onClick: "dismiss" as const,
},
]
showToast({
persistent: true,
icon: "download",
title: language.t("toast.update.title"),
description: language.t("toast.update.description", { version: result.version ?? "" }),
actions,
})
})
.catch((err: unknown) => {
const message = err instanceof Error ? err.message : String(err)
showToast({ title: language.t("common.requestFailed"), description: message })
})
.finally(() => setStore("checking", false))
}
const themeOptions = createMemo(() =>
Object.entries(theme.themes()).map(([id, def]) => ({ id, name: def.name ?? id })),
)
@@ -67,14 +130,8 @@ export const SettingsGeneral: Component = () => {
const soundOptions = [...SOUND_OPTIONS]
return (
<div class="flex flex-col h-full overflow-y-auto no-scrollbar" style={{ padding: "0 40px 40px 40px" }}>
<div
class="sticky top-0 z-10"
style={{
background:
"linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha) calc(100% - 24px), transparent)",
}}
>
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
<div class="flex flex-col gap-1 pt-6 pb-8">
<h2 class="text-16-medium text-text-strong">{language.t("settings.tab.general")}</h2>
</div>
@@ -292,6 +349,50 @@ export const SettingsGeneral: Component = () => {
</SettingsRow>
</div>
</div>
{/* Updates Section */}
<div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.updates")}</h3>
<div class="bg-surface-raised-base px-4 rounded-lg">
<SettingsRow
title={language.t("settings.updates.row.startup.title")}
description={language.t("settings.updates.row.startup.description")}
>
<Switch
checked={settings.updates.startup()}
disabled={!platform.checkUpdate}
onChange={(checked) => settings.updates.setStartup(checked)}
/>
</SettingsRow>
<SettingsRow
title={language.t("settings.general.row.releaseNotes.title")}
description={language.t("settings.general.row.releaseNotes.description")}
>
<Switch
checked={settings.general.releaseNotes()}
onChange={(checked) => settings.general.setReleaseNotes(checked)}
/>
</SettingsRow>
<SettingsRow
title={language.t("settings.updates.row.check.title")}
description={language.t("settings.updates.row.check.description")}
>
<Button
size="small"
variant="secondary"
disabled={store.checking || !platform.checkUpdate}
onClick={check}
>
{store.checking
? language.t("settings.updates.action.checking")
: language.t("settings.updates.action.checkNow")}
</Button>
</SettingsRow>
</div>
</div>
</div>
</div>
)
@@ -305,8 +406,8 @@ interface SettingsRowProps {
const SettingsRow: Component<SettingsRowProps> = (props) => {
return (
<div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
<div class="flex flex-col gap-0.5">
<div class="flex flex-wrap items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
<div class="flex flex-col gap-0.5 min-w-0">
<span class="text-14-medium text-text-strong">{props.title}</span>
<span class="text-12-regular text-text-weak">{props.description}</span>
</div>
@@ -1,4 +1,5 @@
import { Component, For, Show, createMemo, createSignal, onCleanup, onMount } from "solid-js"
import { Component, For, Show, createMemo, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
@@ -111,24 +112,26 @@ export const SettingsKeybinds: Component = () => {
const language = useLanguage()
const settings = useSettings()
const [active, setActive] = createSignal<string | null>(null)
const [filter, setFilter] = createSignal("")
const [store, setStore] = createStore({
active: null as string | null,
filter: "",
})
const stop = () => {
if (!active()) return
setActive(null)
if (!store.active) return
setStore("active", null)
command.keybinds(true)
}
const start = (id: string) => {
if (active() === id) {
if (store.active === id) {
stop()
return
}
if (active()) stop()
if (store.active) stop()
setActive(id)
setStore("active", id)
command.keybinds(false)
}
@@ -203,7 +206,7 @@ export const SettingsKeybinds: Component = () => {
})
const filtered = createMemo(() => {
const query = filter().toLowerCase().trim()
const query = store.filter.toLowerCase().trim()
if (!query) return grouped()
const map = list()
@@ -285,7 +288,7 @@ export const SettingsKeybinds: Component = () => {
onMount(() => {
const handle = (event: KeyboardEvent) => {
const id = active()
const id = store.active
if (!id) return
event.preventDefault()
@@ -345,18 +348,12 @@ export const SettingsKeybinds: Component = () => {
})
onCleanup(() => {
if (active()) command.keybinds(true)
if (store.active) command.keybinds(true)
})
return (
<div class="flex flex-col h-full overflow-y-auto no-scrollbar" style={{ padding: "0 40px 40px 40px" }}>
<div
class="sticky top-0 z-10"
style={{
background:
"linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha) calc(100% - 24px), transparent)",
}}
>
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
<div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
<div class="flex items-center justify-between gap-4">
<h2 class="text-16-medium text-text-strong">{language.t("settings.shortcuts.title")}</h2>
@@ -370,8 +367,8 @@ export const SettingsKeybinds: Component = () => {
<TextField
variant="ghost"
type="text"
value={filter()}
onChange={setFilter}
value={store.filter}
onChange={(v) => setStore("filter", v)}
placeholder={language.t("settings.shortcuts.search.placeholder")}
spellcheck={false}
autocorrect="off"
@@ -379,8 +376,8 @@ export const SettingsKeybinds: Component = () => {
autocapitalize="off"
class="flex-1"
/>
<Show when={filter()}>
<IconButton icon="circle-x" variant="ghost" onClick={() => setFilter("")} />
<Show when={store.filter}>
<IconButton icon="circle-x" variant="ghost" onClick={() => setStore("filter", "")} />
</Show>
</div>
</div>
@@ -402,13 +399,13 @@ export const SettingsKeybinds: Component = () => {
classList={{
"h-8 px-3 rounded-md text-12-regular": true,
"bg-surface-base text-text-subtle hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active":
active() !== id,
"border border-border-weak-base bg-surface-inset-base text-text-weak": active() === id,
store.active !== id,
"border border-border-weak-base bg-surface-inset-base text-text-weak": store.active === id,
}}
onClick={() => start(id)}
>
<Show
when={active() === id}
when={store.active === id}
fallback={command.keybind(id) || language.t("settings.shortcuts.unassigned")}
>
{language.t("settings.shortcuts.pressKeys")}
@@ -423,11 +420,11 @@ export const SettingsKeybinds: Component = () => {
)}
</For>
<Show when={filter() && !hasResults()}>
<Show when={store.filter && !hasResults()}>
<div class="flex flex-col items-center justify-center py-12 text-center">
<span class="text-14-regular text-text-weak">{language.t("settings.shortcuts.search.empty")}</span>
<Show when={filter()}>
<span class="text-14-regular text-text-strong mt-1">"{filter()}"</span>
<Show when={store.filter}>
<span class="text-14-regular text-text-strong mt-1">"{store.filter}"</span>
</Show>
</div>
</Show>
+120 -5
View File
@@ -1,14 +1,129 @@
import { Component } from "solid-js"
import { useFilteredList } from "@opencode-ai/ui/hooks"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Switch } from "@opencode-ai/ui/switch"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { TextField } from "@opencode-ai/ui/text-field"
import type { IconName } from "@opencode-ai/ui/icons/provider"
import { type Component, For, Show } from "solid-js"
import { useLanguage } from "@/context/language"
import { useModels } from "@/context/models"
import { popularProviders } from "@/hooks/use-providers"
type ModelItem = ReturnType<ReturnType<typeof useModels>["list"]>[number]
export const SettingsModels: Component = () => {
const language = useLanguage()
const models = useModels()
const list = useFilteredList<ModelItem>({
items: (_filter) => models.list(),
key: (x) => `${x.provider.id}:${x.id}`,
filterKeys: ["provider.name", "name", "id"],
sortBy: (a, b) => a.name.localeCompare(b.name),
groupBy: (x) => x.provider.id,
sortGroupsBy: (a, b) => {
const aIndex = popularProviders.indexOf(a.category)
const bIndex = popularProviders.indexOf(b.category)
const aPopular = aIndex >= 0
const bPopular = bIndex >= 0
if (aPopular && !bPopular) return -1
if (!aPopular && bPopular) return 1
if (aPopular && bPopular) return aIndex - bIndex
const aName = a.items[0].provider.name
const bName = b.items[0].provider.name
return aName.localeCompare(bName)
},
})
return (
<div class="flex flex-col h-full overflow-y-auto">
<div class="flex flex-col gap-6 p-6 max-w-[600px]">
<h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
<p class="text-14-regular text-text-weak">{language.t("settings.models.description")}</p>
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
<div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
<h2 class="text-16-medium text-text-strong">{language.t("settings.models.title")}</h2>
<div class="flex items-center gap-2 px-3 h-9 rounded-lg bg-surface-base">
<Icon name="magnifying-glass" class="text-icon-weak-base flex-shrink-0" />
<TextField
variant="ghost"
type="text"
value={list.filter()}
onChange={list.onInput}
placeholder={language.t("dialog.model.search.placeholder")}
spellcheck={false}
autocorrect="off"
autocomplete="off"
autocapitalize="off"
class="flex-1"
/>
<Show when={list.filter()}>
<IconButton icon="circle-x" variant="ghost" onClick={list.clear} />
</Show>
</div>
</div>
</div>
<div class="flex flex-col gap-8 max-w-[720px]">
<Show
when={!list.grouped.loading}
fallback={
<div class="flex flex-col items-center justify-center py-12 text-center">
<span class="text-14-regular text-text-weak">
{language.t("common.loading")}
{language.t("common.loading.ellipsis")}
</span>
</div>
}
>
<Show
when={list.flat().length > 0}
fallback={
<div class="flex flex-col items-center justify-center py-12 text-center">
<span class="text-14-regular text-text-weak">{language.t("dialog.model.empty")}</span>
<Show when={list.filter()}>
<span class="text-14-regular text-text-strong mt-1">&quot;{list.filter()}&quot;</span>
</Show>
</div>
}
>
<For each={list.grouped.latest}>
{(group) => (
<div class="flex flex-col gap-1">
<div class="flex items-center gap-2 pb-2">
<ProviderIcon id={group.category as IconName} class="size-5 shrink-0 icon-strong-base" />
<span class="text-14-medium text-text-strong">{group.items[0].provider.name}</span>
</div>
<div class="bg-surface-raised-base px-4 rounded-lg">
<For each={group.items}>
{(item) => {
const key = { providerID: item.provider.id, modelID: item.id }
return (
<div class="flex flex-wrap items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
<div class="min-w-0">
<span class="text-14-regular text-text-strong truncate block">{item.name}</span>
</div>
<div class="flex-shrink-0">
<Switch
checked={models.visible(key)}
onChange={(checked) => {
models.setVisibility(key, checked)
}}
hideLabel
>
{item.name}
</Switch>
</div>
</div>
)
}}
</For>
</div>
</div>
)}
</For>
</Show>
</Show>
</div>
</div>
)
@@ -175,20 +175,14 @@ export const SettingsPermissions: Component = () => {
return (
<div class="flex flex-col h-full overflow-y-auto no-scrollbar">
<div
class="sticky top-0 z-10"
style={{
background:
"linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha) calc(100% - 24px), transparent)",
}}
>
<div class="flex flex-col gap-1 p-8 max-w-[720px]">
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
<div class="flex flex-col gap-1 px-4 py-8 sm:p-8 max-w-[720px]">
<h2 class="text-16-medium text-text-strong">{language.t("settings.permissions.title")}</h2>
<p class="text-14-regular text-text-weak">{language.t("settings.permissions.description")}</p>
</div>
</div>
<div class="flex flex-col gap-6 p-8 pt-6 max-w-[720px]">
<div class="flex flex-col gap-6 px-4 py-6 sm:p-8 sm:pt-6 max-w-[720px]">
<div class="flex flex-col gap-2">
<h3 class="text-14-medium text-text-strong">{language.t("settings.permissions.section.tools")}</h3>
<div class="border border-border-weak-base rounded-lg overflow-hidden">
@@ -223,8 +217,8 @@ interface SettingsRowProps {
const SettingsRow: Component<SettingsRowProps> = (props) => {
return (
<div class="flex items-center justify-between gap-4 px-4 py-3 border-b border-border-weak-base last:border-none">
<div class="flex flex-col gap-0.5">
<div class="flex flex-wrap items-center justify-between gap-4 px-4 py-3 border-b border-border-weak-base last:border-none">
<div class="flex flex-col gap-0.5 min-w-0">
<span class="text-14-medium text-text-strong">{props.title}</span>
<span class="text-12-regular text-text-weak">{props.description}</span>
</div>
@@ -1,14 +1,262 @@
import { Component } from "solid-js"
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Tag } from "@opencode-ai/ui/tag"
import { showToast } from "@opencode-ai/ui/toast"
import { iconNames, type IconName } from "@opencode-ai/ui/icons/provider"
import { popularProviders, useProviders } from "@/hooks/use-providers"
import { createMemo, type Component, For, Show } from "solid-js"
import { useLanguage } from "@/context/language"
import { useGlobalSDK } from "@/context/global-sdk"
import { useGlobalSync } from "@/context/global-sync"
import { DialogConnectProvider } from "./dialog-connect-provider"
import { DialogSelectProvider } from "./dialog-select-provider"
import { DialogCustomProvider } from "./dialog-custom-provider"
type ProviderSource = "env" | "api" | "config" | "custom"
type ProviderMeta = { source?: ProviderSource }
export const SettingsProviders: Component = () => {
const dialog = useDialog()
const language = useLanguage()
const globalSDK = useGlobalSDK()
const globalSync = useGlobalSync()
const providers = useProviders()
const icon = (id: string): IconName => {
if (iconNames.includes(id as IconName)) return id as IconName
return "synthetic"
}
const connected = createMemo(() => {
return providers
.connected()
.filter((p) => p.id !== "opencode" || Object.values(p.models).find((m) => m.cost?.input))
})
const popular = createMemo(() => {
const connectedIDs = new Set(connected().map((p) => p.id))
const items = providers
.popular()
.filter((p) => !connectedIDs.has(p.id))
.slice()
items.sort((a, b) => popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id))
return items
})
const source = (item: unknown) => (item as ProviderMeta).source
const type = (item: unknown) => {
const current = source(item)
if (current === "env") return language.t("settings.providers.tag.environment")
if (current === "api") return language.t("provider.connect.method.apiKey")
if (current === "config") {
const id = (item as { id?: string }).id
if (id && isConfigCustom(id)) return language.t("settings.providers.tag.custom")
return language.t("settings.providers.tag.config")
}
if (current === "custom") return language.t("settings.providers.tag.custom")
return language.t("settings.providers.tag.other")
}
const canDisconnect = (item: unknown) => source(item) !== "env"
const isConfigCustom = (providerID: string) => {
const provider = globalSync.data.config.provider?.[providerID]
if (!provider) return false
if (provider.npm !== "@ai-sdk/openai-compatible") return false
if (!provider.models || Object.keys(provider.models).length === 0) return false
return true
}
const disableProvider = async (providerID: string, name: string) => {
const before = globalSync.data.config.disabled_providers ?? []
const next = before.includes(providerID) ? before : [...before, providerID]
globalSync.set("config", "disabled_providers", next)
await globalSync
.updateConfig({ disabled_providers: next })
.then(() => {
showToast({
variant: "success",
icon: "circle-check",
title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }),
description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }),
})
})
.catch((err: unknown) => {
globalSync.set("config", "disabled_providers", before)
const message = err instanceof Error ? err.message : String(err)
showToast({ title: language.t("common.requestFailed"), description: message })
})
}
const disconnect = async (providerID: string, name: string) => {
if (isConfigCustom(providerID)) {
await globalSDK.client.auth.remove({ providerID }).catch(() => undefined)
await disableProvider(providerID, name)
return
}
await globalSDK.client.auth
.remove({ providerID })
.then(async () => {
await globalSDK.client.global.dispose()
showToast({
variant: "success",
icon: "circle-check",
title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }),
description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }),
})
})
.catch((err: unknown) => {
const message = err instanceof Error ? err.message : String(err)
showToast({ title: language.t("common.requestFailed"), description: message })
})
}
return (
<div class="flex flex-col h-full overflow-y-auto">
<div class="flex flex-col gap-6 p-6 max-w-[600px]">
<h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
<p class="text-14-regular text-text-weak">{language.t("settings.providers.description")}</p>
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
<div class="flex flex-col gap-1 pt-6 pb-8 max-w-[720px]">
<h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
</div>
</div>
<div class="flex flex-col gap-8 max-w-[720px]">
<div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.connected")}</h3>
<div class="bg-surface-raised-base px-4 rounded-lg">
<Show
when={connected().length > 0}
fallback={
<div class="py-4 text-14-regular text-text-weak">
{language.t("settings.providers.connected.empty")}
</div>
}
>
<For each={connected()}>
{(item) => (
<div class="group flex flex-wrap items-center justify-between gap-4 min-h-16 py-3 border-b border-border-weak-base last:border-none">
<div class="flex items-center gap-3 min-w-0">
<ProviderIcon id={icon(item.id)} class="size-5 shrink-0 icon-strong-base" />
<span class="text-14-medium text-text-strong truncate">{item.name}</span>
<Tag>{type(item)}</Tag>
</div>
<Show
when={canDisconnect(item)}
fallback={
<span class="text-14-regular text-text-base opacity-0 group-hover:opacity-100 transition-opacity duration-200 pr-3 cursor-default">
Connected from your environment variables
</span>
}
>
<Button size="large" variant="ghost" onClick={() => void disconnect(item.id, item.name)}>
{language.t("common.disconnect")}
</Button>
</Show>
</div>
)}
</For>
</Show>
</div>
</div>
<div class="flex flex-col gap-1">
<h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.popular")}</h3>
<div class="bg-surface-raised-base px-4 rounded-lg">
<For each={popular()}>
{(item) => (
<div class="flex flex-wrap items-center justify-between gap-4 min-h-16 py-3 border-b border-border-weak-base last:border-none">
<div class="flex flex-col min-w-0">
<div class="flex items-center gap-x-3">
<ProviderIcon id={icon(item.id)} class="size-5 shrink-0 icon-strong-base" />
<span class="text-14-medium text-text-strong">{item.name}</span>
<Show when={item.id === "opencode"}>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</Show>
</div>
<Show when={item.id === "opencode"}>
<span class="text-12-regular text-text-weak pl-8">
{language.t("dialog.provider.opencode.note")}
</span>
</Show>
<Show when={item.id === "anthropic"}>
<span class="text-12-regular text-text-weak pl-8">
{language.t("dialog.provider.anthropic.note")}
</span>
</Show>
<Show when={item.id.startsWith("github-copilot")}>
<span class="text-12-regular text-text-weak pl-8">
{language.t("dialog.provider.copilot.note")}
</span>
</Show>
<Show when={item.id === "openai"}>
<span class="text-12-regular text-text-weak pl-8">
{language.t("dialog.provider.openai.note")}
</span>
</Show>
<Show when={item.id === "google"}>
<span class="text-12-regular text-text-weak pl-8">
{language.t("dialog.provider.google.note")}
</span>
</Show>
<Show when={item.id === "openrouter"}>
<span class="text-12-regular text-text-weak pl-8">
{language.t("dialog.provider.openrouter.note")}
</span>
</Show>
<Show when={item.id === "vercel"}>
<span class="text-12-regular text-text-weak pl-8">
{language.t("dialog.provider.vercel.note")}
</span>
</Show>
</div>
<Button
size="large"
variant="secondary"
icon="plus-small"
onClick={() => {
dialog.show(() => <DialogConnectProvider provider={item.id} />)
}}
>
{language.t("common.connect")}
</Button>
</div>
)}
</For>
<div class="flex items-center justify-between gap-4 h-16 border-b border-border-weak-base last:border-none">
<div class="flex flex-col min-w-0">
<div class="flex items-center gap-x-3">
<ProviderIcon id={icon("synthetic")} class="size-5 shrink-0 icon-strong-base" />
<span class="text-14-medium text-text-strong">Custom provider</span>
<Tag>{language.t("settings.providers.tag.custom")}</Tag>
</div>
<span class="text-12-regular text-text-weak pl-8">Add an OpenAI-compatible provider by base URL.</span>
</div>
<Button
size="large"
variant="secondary"
icon="plus-small"
onClick={() => {
dialog.show(() => <DialogCustomProvider back="close" />)
}}
>
{language.t("common.connect")}
</Button>
</div>
</div>
<Button
variant="ghost"
class="px-0 py-0 mt-5 text-14-medium text-text-interactive-base text-left justify-start hover:bg-transparent active:bg-transparent"
onClick={() => {
dialog.show(() => <DialogSelectProvider />)
}}
>
{language.t("dialog.provider.viewAll")}
</Button>
</div>
</div>
</div>
)
+69 -54
View File
@@ -15,14 +15,16 @@ import { usePlatform } from "@/context/platform"
import { useLanguage } from "@/context/language"
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
import { DialogSelectServer } from "./dialog-select-server"
import { showToast } from "@opencode-ai/ui/toast"
type ServerStatus = { healthy: boolean; version?: string }
async function checkHealth(url: string, platform: ReturnType<typeof usePlatform>): Promise<ServerStatus> {
const signal = (AbortSignal as unknown as { timeout?: (ms: number) => AbortSignal }).timeout?.(3000)
const sdk = createOpencodeClient({
baseUrl: url,
fetch: platform.fetch,
signal: AbortSignal.timeout(3000),
signal,
})
return sdk.global
.health()
@@ -39,9 +41,10 @@ export function StatusPopover() {
const language = useLanguage()
const navigate = useNavigate()
const [loading, setLoading] = createSignal<string | null>(null)
const [store, setStore] = createStore({
status: {} as Record<string, ServerStatus | undefined>,
loading: null as string | null,
defaultServerUrl: undefined as string | undefined,
})
const servers = createMemo(() => {
@@ -97,17 +100,23 @@ export function StatusPopover() {
const mcpConnected = createMemo(() => mcpItems().filter((i) => i.status === "connected").length)
const toggleMcp = async (name: string) => {
if (loading()) return
setLoading(name)
const status = sync.data.mcp[name]
if (status?.status === "connected") {
await sdk.client.mcp.disconnect({ name })
} else {
await sdk.client.mcp.connect({ name })
if (store.loading) return
setStore("loading", name)
try {
const status = sync.data.mcp[name]
await (status?.status === "connected" ? sdk.client.mcp.disconnect({ name }) : sdk.client.mcp.connect({ name }))
const result = await sdk.client.mcp.status()
if (result.data) sync.set("mcp", result.data)
} catch (err) {
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: err instanceof Error ? err.message : String(err),
})
} finally {
setStore("loading", null)
}
const result = await sdk.client.mcp.status()
if (result.data) sync.set("mcp", result.data)
setLoading(null)
}
const lspItems = createMemo(() => sync.data.lsp ?? [])
@@ -123,15 +132,21 @@ export function StatusPopover() {
const serverCount = createMemo(() => sortedServers().length)
const [defaultServerUrl, setDefaultServerUrl] = createSignal<string | undefined>()
createEffect(() => {
const refreshDefaultServerUrl = () => {
const result = platform.getDefaultServerUrl?.()
if (result instanceof Promise) {
result.then((url) => setDefaultServerUrl(url ? normalizeServerUrl(url) : undefined))
if (!result) {
setStore("defaultServerUrl", undefined)
return
}
if (result) setDefaultServerUrl(normalizeServerUrl(result))
if (result instanceof Promise) {
result.then((url) => setStore("defaultServerUrl", url ? normalizeServerUrl(url) : undefined))
return
}
setStore("defaultServerUrl", normalizeServerUrl(result))
}
createEffect(() => {
refreshDefaultServerUrl()
})
return (
@@ -153,7 +168,7 @@ export function StatusPopover() {
"bg-border-weak-base": server.healthy() === undefined,
}}
/>
<span class="text-12-regular text-text-strong">Status</span>
<span class="text-12-regular text-text-strong">{language.t("status.popover.trigger")}</span>
</div>
}
class="[&_[data-slot=popover-body]]:p-0 w-[360px] max-w-[calc(100vw-40px)] bg-transparent border-0 shadow-none rounded-xl"
@@ -161,44 +176,31 @@ export function StatusPopover() {
placement="bottom-end"
shift={-136}
>
<div
class="flex items-center gap-1 w-[360px] rounded-xl"
style={{ "box-shadow": "var(--shadow-lg-border-base)" }}
>
<div class="flex items-center gap-1 w-[360px] rounded-xl shadow-[var(--shadow-lg-border-base)]">
<Tabs
aria-label="Server Configurations"
class="tabs"
aria-label={language.t("status.popover.ariaLabel")}
class="tabs bg-background-strong rounded-xl overflow-hidden"
data-component="tabs"
data-active="servers"
defaultValue="servers"
variant="alt"
style={{
"background-color": "var(--background-strong)",
"border-radius": "12px",
overflow: "hidden",
}}
>
<Tabs.List
data-slot="tablist"
style={{
"background-color": "transparent",
"border-bottom": "none",
padding: "8px 16px 0",
gap: "16px",
height: "40px",
}}
>
<Tabs.List data-slot="tablist" class="bg-transparent border-b-0 px-4 pt-2 pb-0 gap-4 h-10">
<Tabs.Trigger value="servers" data-slot="tab" class="text-12-regular">
{serverCount() > 0 ? `${serverCount()} ` : ""}Servers
{serverCount() > 0 ? `${serverCount()} ` : ""}
{language.t("status.popover.tab.servers")}
</Tabs.Trigger>
<Tabs.Trigger value="mcp" data-slot="tab" class="text-12-regular">
{mcpConnected() > 0 ? `${mcpConnected()} ` : ""}MCP
{mcpConnected() > 0 ? `${mcpConnected()} ` : ""}
{language.t("status.popover.tab.mcp")}
</Tabs.Trigger>
<Tabs.Trigger value="lsp" data-slot="tab" class="text-12-regular">
{lspCount() > 0 ? `${lspCount()} ` : ""}LSP
{lspCount() > 0 ? `${lspCount()} ` : ""}
{language.t("status.popover.tab.lsp")}
</Tabs.Trigger>
<Tabs.Trigger value="plugins" data-slot="tab" class="text-12-regular">
{pluginCount() > 0 ? `${pluginCount()} ` : ""}Plugins
{pluginCount() > 0 ? `${pluginCount()} ` : ""}
{language.t("status.popover.tab.plugins")}
</Tabs.Trigger>
</Tabs.List>
@@ -208,7 +210,7 @@ export function StatusPopover() {
<For each={sortedServers()}>
{(url) => {
const isActive = () => url === server.url
const isDefault = () => url === defaultServerUrl()
const isDefault = () => url === store.defaultServerUrl
const status = () => store.status[url]
const isBlocked = () => status()?.healthy === false
const [truncated, setTruncated] = createSignal(false)
@@ -274,7 +276,7 @@ export function StatusPopover() {
</Show>
<Show when={isDefault()}>
<span class="text-11-regular text-text-base bg-surface-base px-1.5 py-0.5 rounded-md">
Default
{language.t("common.default")}
</span>
</Show>
<div class="flex-1" />
@@ -290,9 +292,9 @@ export function StatusPopover() {
<Button
variant="secondary"
class="mt-3 self-start h-8 px-3 py-1.5"
onClick={() => dialog.show(() => <DialogSelectServer />)}
onClick={() => dialog.show(() => <DialogSelectServer />, refreshDefaultServerUrl)}
>
Manage servers
{language.t("status.popover.action.manageServers")}
</Button>
</div>
</div>
@@ -304,7 +306,9 @@ export function StatusPopover() {
<Show
when={mcpItems().length > 0}
fallback={
<div class="text-14-regular text-text-base text-center my-auto">No MCP servers configured</div>
<div class="text-14-regular text-text-base text-center my-auto">
{language.t("dialog.mcp.empty")}
</div>
}
>
<For each={mcpItems()}>
@@ -315,7 +319,7 @@ export function StatusPopover() {
type="button"
class="flex items-center gap-2 w-full h-8 pl-3 pr-2 py-1 rounded-md hover:bg-surface-raised-base-hover transition-colors text-left"
onClick={() => toggleMcp(item.name)}
disabled={loading() === item.name}
disabled={store.loading === item.name}
>
<div
classList={{
@@ -331,7 +335,7 @@ export function StatusPopover() {
<div onClick={(event) => event.stopPropagation()}>
<Switch
checked={enabled()}
disabled={loading() === item.name}
disabled={store.loading === item.name}
onChange={() => toggleMcp(item.name)}
/>
</div>
@@ -351,7 +355,7 @@ export function StatusPopover() {
when={lspItems().length > 0}
fallback={
<div class="text-14-regular text-text-base text-center my-auto">
LSPs auto-detected from file types
{language.t("dialog.lsp.empty")}
</div>
}
>
@@ -381,8 +385,19 @@ export function StatusPopover() {
when={plugins().length > 0}
fallback={
<div class="text-14-regular text-text-base text-center my-auto">
Plugins configured in{" "}
<code class="bg-surface-raised-base px-1.5 py-0.5 rounded-sm text-text-base">opencode.json</code>
{(() => {
const value = language.t("dialog.plugins.empty")
const file = "opencode.json"
const parts = value.split(file)
if (parts.length === 1) return value
return (
<>
{parts[0]}
<code class="bg-surface-raised-base px-1.5 py-0.5 rounded-sm text-text-base">{file}</code>
{parts.slice(1).join(file)}
</>
)
})()}
</div>
}
>
+237 -161
View File
@@ -5,6 +5,8 @@ import { monoFontFamily, useSettings } from "@/context/settings"
import { SerializeAddon } from "@/addons/serialize"
import { LocalPTY } from "@/context/terminal"
import { resolveThemeVariant, useTheme, withAlpha, type HexColor } from "@opencode-ai/ui/theme"
import { useLanguage } from "@/context/language"
import { showToast } from "@opencode-ai/ui/toast"
export interface TerminalProps extends ComponentProps<"div"> {
pty: LocalPTY
@@ -14,6 +16,19 @@ export interface TerminalProps extends ComponentProps<"div"> {
onConnectError?: (error: unknown) => void
}
let shared: Promise<{ mod: typeof import("ghostty-web"); ghostty: Ghostty }> | undefined
const loadGhostty = () => {
if (shared) return shared
shared = import("ghostty-web")
.then(async (mod) => ({ mod, ghostty: await mod.Ghostty.load() }))
.catch((err) => {
shared = undefined
throw err
})
return shared
}
type TerminalColors = {
background: string
foreground: string
@@ -40,6 +55,7 @@ export const Terminal = (props: TerminalProps) => {
const sdk = useSDK()
const settings = useSettings()
const theme = useTheme()
const language = useLanguage()
let container!: HTMLDivElement
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnect", "onConnectError"])
let ws: WebSocket | undefined
@@ -50,8 +66,20 @@ export const Terminal = (props: TerminalProps) => {
let handleResize: () => void
let handleTextareaFocus: () => void
let handleTextareaBlur: () => void
let reconnect: number | undefined
let disposed = false
const cleanups: VoidFunction[] = []
const cleanup = () => {
if (!cleanups.length) return
const fns = cleanups.splice(0).reverse()
for (const fn of fns) {
try {
fn()
} catch {
// ignore
}
}
}
const getTerminalColors = (): TerminalColors => {
const mode = theme.mode()
@@ -107,188 +135,237 @@ export const Terminal = (props: TerminalProps) => {
focusTerminal()
}
onMount(async () => {
const mod = await import("ghostty-web")
ghostty = await mod.Ghostty.load()
onMount(() => {
const run = async () => {
const loaded = await loadGhostty()
if (disposed) return
const once = { value: false }
const mod = loaded.mod
const g = loaded.ghostty
const url = new URL(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
if (window.__OPENCODE__?.serverPassword) {
url.username = "opencode"
url.password = window.__OPENCODE__?.serverPassword
}
const socket = new WebSocket(url)
ws = socket
const once = { value: false }
const t = new mod.Terminal({
cursorBlink: true,
cursorStyle: "bar",
fontSize: 14,
fontFamily: monoFontFamily(settings.appearance.font()),
allowTransparency: true,
theme: terminalColors(),
scrollback: 10_000,
ghostty,
})
term = t
const copy = () => {
const selection = t.getSelection()
if (!selection) return false
const body = document.body
if (body) {
const textarea = document.createElement("textarea")
textarea.value = selection
textarea.setAttribute("readonly", "")
textarea.style.position = "fixed"
textarea.style.opacity = "0"
body.appendChild(textarea)
textarea.select()
const copied = document.execCommand("copy")
body.removeChild(textarea)
if (copied) return true
const url = new URL(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
if (window.__OPENCODE__?.serverPassword) {
url.username = "opencode"
url.password = window.__OPENCODE__?.serverPassword
}
const clipboard = navigator.clipboard
if (clipboard?.writeText) {
clipboard.writeText(selection).catch(() => {})
return true
}
return false
}
t.attachCustomKeyEventHandler((event) => {
const key = event.key.toLowerCase()
if (event.ctrlKey && event.shiftKey && !event.metaKey && key === "c") {
copy()
return true
}
if (event.metaKey && !event.ctrlKey && !event.altKey && key === "c") {
if (!t.hasSelection()) return true
copy()
return true
}
// allow for ctrl-` to toggle terminal in parent
if (event.ctrlKey && key === "`") {
return true
}
return false
})
fitAddon = new mod.FitAddon()
serializeAddon = new SerializeAddon()
t.loadAddon(serializeAddon)
t.loadAddon(fitAddon)
t.open(container)
container.addEventListener("pointerdown", handlePointerDown)
handleTextareaFocus = () => {
t.options.cursorBlink = true
}
handleTextareaBlur = () => {
t.options.cursorBlink = false
}
t.textarea?.addEventListener("focus", handleTextareaFocus)
t.textarea?.addEventListener("blur", handleTextareaBlur)
focusTerminal()
if (local.pty.buffer) {
if (local.pty.rows && local.pty.cols) {
t.resize(local.pty.cols, local.pty.rows)
}
t.write(local.pty.buffer, () => {
if (local.pty.scrollY) {
t.scrollToLine(local.pty.scrollY)
}
fitAddon.fit()
const socket = new WebSocket(url)
cleanups.push(() => {
if (socket.readyState !== WebSocket.CLOSED && socket.readyState !== WebSocket.CLOSING) socket.close()
})
}
if (disposed) {
cleanup()
return
}
ws = socket
fitAddon.observeResize()
handleResize = () => fitAddon.fit()
window.addEventListener("resize", handleResize)
t.onResize(async (size) => {
if (socket.readyState === WebSocket.OPEN) {
await sdk.client.pty
const t = new mod.Terminal({
cursorBlink: true,
cursorStyle: "bar",
fontSize: 14,
fontFamily: monoFontFamily(settings.appearance.font()),
allowTransparency: true,
theme: terminalColors(),
scrollback: 10_000,
ghostty: g,
})
cleanups.push(() => t.dispose())
if (disposed) {
cleanup()
return
}
ghostty = g
term = t
const copy = () => {
const selection = t.getSelection()
if (!selection) return false
const body = document.body
if (body) {
const textarea = document.createElement("textarea")
textarea.value = selection
textarea.setAttribute("readonly", "")
textarea.style.position = "fixed"
textarea.style.opacity = "0"
body.appendChild(textarea)
textarea.select()
const copied = document.execCommand("copy")
body.removeChild(textarea)
if (copied) return true
}
const clipboard = navigator.clipboard
if (clipboard?.writeText) {
clipboard.writeText(selection).catch(() => {})
return true
}
return false
}
t.attachCustomKeyEventHandler((event) => {
const key = event.key.toLowerCase()
if (event.ctrlKey && event.shiftKey && !event.metaKey && key === "c") {
copy()
return true
}
if (event.metaKey && !event.ctrlKey && !event.altKey && key === "c") {
if (!t.hasSelection()) return true
copy()
return true
}
// allow for ctrl-` to toggle terminal in parent
if (event.ctrlKey && key === "`") {
return true
}
return false
})
const fit = new mod.FitAddon()
const serializer = new SerializeAddon()
cleanups.push(() => (fit as unknown as { dispose?: VoidFunction }).dispose?.())
t.loadAddon(serializer)
t.loadAddon(fit)
fitAddon = fit
serializeAddon = serializer
t.open(container)
container.addEventListener("pointerdown", handlePointerDown)
cleanups.push(() => container.removeEventListener("pointerdown", handlePointerDown))
handleTextareaFocus = () => {
t.options.cursorBlink = true
}
handleTextareaBlur = () => {
t.options.cursorBlink = false
}
t.textarea?.addEventListener("focus", handleTextareaFocus)
t.textarea?.addEventListener("blur", handleTextareaBlur)
cleanups.push(() => t.textarea?.removeEventListener("focus", handleTextareaFocus))
cleanups.push(() => t.textarea?.removeEventListener("blur", handleTextareaBlur))
focusTerminal()
if (local.pty.buffer) {
if (local.pty.rows && local.pty.cols) {
t.resize(local.pty.cols, local.pty.rows)
}
t.write(local.pty.buffer, () => {
if (local.pty.scrollY) {
t.scrollToLine(local.pty.scrollY)
}
fitAddon.fit()
})
}
fit.observeResize()
handleResize = () => fit.fit()
window.addEventListener("resize", handleResize)
cleanups.push(() => window.removeEventListener("resize", handleResize))
const onResize = t.onResize(async (size) => {
if (socket.readyState === WebSocket.OPEN) {
await sdk.client.pty
.update({
ptyID: local.pty.id,
size: {
cols: size.cols,
rows: size.rows,
},
})
.catch(() => {})
}
})
cleanups.push(() => (onResize as unknown as { dispose?: VoidFunction }).dispose?.())
const onData = t.onData((data) => {
if (socket.readyState === WebSocket.OPEN) {
socket.send(data)
}
})
cleanups.push(() => (onData as unknown as { dispose?: VoidFunction }).dispose?.())
const onKey = t.onKey((key) => {
if (key.key == "Enter") {
props.onSubmit?.()
}
})
cleanups.push(() => (onKey as unknown as { dispose?: VoidFunction }).dispose?.())
// t.onScroll((ydisp) => {
// console.log("Scroll position:", ydisp)
// })
const handleOpen = () => {
local.onConnect?.()
sdk.client.pty
.update({
ptyID: local.pty.id,
size: {
cols: size.cols,
rows: size.rows,
cols: t.cols,
rows: t.rows,
},
})
.catch(() => {})
}
})
t.onData((data) => {
if (socket.readyState === WebSocket.OPEN) {
socket.send(data)
socket.addEventListener("open", handleOpen)
cleanups.push(() => socket.removeEventListener("open", handleOpen))
const handleMessage = (event: MessageEvent) => {
t.write(event.data)
}
})
t.onKey((key) => {
if (key.key == "Enter") {
props.onSubmit?.()
}
})
// t.onScroll((ydisp) => {
// console.log("Scroll position:", ydisp)
// })
socket.addEventListener("open", () => {
local.onConnect?.()
sdk.client.pty
.update({
ptyID: local.pty.id,
size: {
cols: t.cols,
rows: t.rows,
},
})
.catch(() => {})
})
socket.addEventListener("message", (event) => {
t.write(event.data)
})
socket.addEventListener("error", (error) => {
if (disposed) return
if (once.value) return
once.value = true
console.error("WebSocket error:", error)
local.onConnectError?.(error)
})
socket.addEventListener("close", (event) => {
if (disposed) return
// Normal closure (code 1000) means PTY process exited - server event handles cleanup
// For other codes (network issues, server restart), trigger error handler
if (event.code !== 1000) {
socket.addEventListener("message", handleMessage)
cleanups.push(() => socket.removeEventListener("message", handleMessage))
const handleError = (error: Event) => {
if (disposed) return
if (once.value) return
once.value = true
local.onConnectError?.(new Error(`WebSocket closed abnormally: ${event.code}`))
console.error("WebSocket error:", error)
local.onConnectError?.(error)
}
socket.addEventListener("error", handleError)
cleanups.push(() => socket.removeEventListener("error", handleError))
const handleClose = (event: CloseEvent) => {
if (disposed) return
// Normal closure (code 1000) means PTY process exited - server event handles cleanup
// For other codes (network issues, server restart), trigger error handler
if (event.code !== 1000) {
if (once.value) return
once.value = true
local.onConnectError?.(new Error(`WebSocket closed abnormally: ${event.code}`))
}
}
socket.addEventListener("close", handleClose)
cleanups.push(() => socket.removeEventListener("close", handleClose))
}
void run().catch((err) => {
if (disposed) return
showToast({
variant: "error",
title: language.t("terminal.connectionLost.title"),
description: err instanceof Error ? err.message : language.t("terminal.connectionLost.description"),
})
local.onConnectError?.(err)
})
})
onCleanup(() => {
disposed = true
if (handleResize) {
window.removeEventListener("resize", handleResize)
}
container.removeEventListener("pointerdown", handlePointerDown)
term?.textarea?.removeEventListener("focus", handleTextareaFocus)
term?.textarea?.removeEventListener("blur", handleTextareaBlur)
const t = term
if (serializeAddon && props.onCleanup && t) {
const buffer = serializeAddon.serialize()
const buffer = (() => {
try {
return serializeAddon.serialize()
} catch {
return ""
}
})()
props.onCleanup({
...local.pty,
buffer,
@@ -298,8 +375,7 @@ export const Terminal = (props: TerminalProps) => {
})
}
ws?.close()
t?.dispose()
cleanup()
})
return (
+110 -27
View File
@@ -1,8 +1,10 @@
import { createEffect, createMemo, Show } from "solid-js"
import { createEffect, createMemo, Show, untrack } from "solid-js"
import { createStore } from "solid-js/store"
import { useLocation, useNavigate } from "@solidjs/router"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon } from "@opencode-ai/ui/icon"
import { Button } from "@opencode-ai/ui/button"
import { TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { useTheme } from "@opencode-ai/ui/theme"
import { useLayout } from "@/context/layout"
@@ -16,11 +18,68 @@ export function Titlebar() {
const command = useCommand()
const language = useLanguage()
const theme = useTheme()
const navigate = useNavigate()
const location = useLocation()
const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
const web = createMemo(() => platform.platform === "web")
const [history, setHistory] = createStore({
stack: [] as string[],
index: 0,
action: undefined as "back" | "forward" | undefined,
})
const path = () => `${location.pathname}${location.search}${location.hash}`
createEffect(() => {
const current = path()
untrack(() => {
if (!history.stack.length) {
const stack = current === "/" ? ["/"] : ["/", current]
setHistory({ stack, index: stack.length - 1 })
return
}
const active = history.stack[history.index]
if (current === active) {
if (history.action) setHistory("action", undefined)
return
}
if (history.action) {
setHistory("action", undefined)
return
}
const next = history.stack.slice(0, history.index + 1).concat(current)
setHistory({ stack: next, index: next.length - 1 })
})
})
const canBack = createMemo(() => history.index > 0)
const canForward = createMemo(() => history.index < history.stack.length - 1)
const back = () => {
if (!canBack()) return
const index = history.index - 1
const to = history.stack[index]
if (!to) return
setHistory({ index, action: "back" })
navigate(to)
}
const forward = () => {
if (!canForward()) return
const index = history.index + 1
const to = history.stack[index]
if (!to) return
setHistory({ index, action: "forward" })
navigate(to)
}
const getWin = () => {
if (platform.platform !== "desktop") return
@@ -106,34 +165,58 @@ export function Titlebar() {
/>
</div>
</Show>
<TooltipKeybind
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
placement="bottom"
title={language.t("command.sidebar.toggle")}
keybind={command.keybind("sidebar.toggle")}
>
<Button
variant="ghost"
class="group/sidebar-toggle size-6 p-0"
onClick={layout.sidebar.toggle}
aria-label={language.t("command.sidebar.toggle")}
aria-expanded={layout.sidebar.opened()}
<div class="flex items-center gap-3 shrink-0">
<TooltipKeybind
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0 ml-2"}
placement="bottom"
title={language.t("command.sidebar.toggle")}
keybind={command.keybind("sidebar.toggle")}
>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
size="small"
name={layout.sidebar.opened() ? "layout-left-full" : "layout-left"}
class="group-hover/sidebar-toggle:hidden"
<Button
variant="ghost"
class="group/sidebar-toggle size-6 p-0"
onClick={layout.sidebar.toggle}
aria-label={language.t("command.sidebar.toggle")}
aria-expanded={layout.sidebar.opened()}
>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
size="small"
name={layout.sidebar.opened() ? "layout-left-full" : "layout-left"}
class="group-hover/sidebar-toggle:hidden"
/>
<Icon size="small" name="layout-left-partial" class="hidden group-hover/sidebar-toggle:inline-block" />
<Icon
size="small"
name={layout.sidebar.opened() ? "layout-left" : "layout-left-full"}
class="hidden group-active/sidebar-toggle:inline-block"
/>
</div>
</Button>
</TooltipKeybind>
<div class="hidden xl:flex items-center gap-1 shrink-0">
<Tooltip placement="bottom" value={language.t("common.goBack")} openDelay={2000}>
<Button
variant="ghost"
icon="arrow-left"
class="size-6 p-0"
disabled={!canBack()}
onClick={back}
aria-label={language.t("common.goBack")}
/>
<Icon size="small" name="layout-left-partial" class="hidden group-hover/sidebar-toggle:inline-block" />
<Icon
size="small"
name={layout.sidebar.opened() ? "layout-left" : "layout-left-full"}
class="hidden group-active/sidebar-toggle:inline-block"
</Tooltip>
<Tooltip placement="bottom" value={language.t("common.goForward")} openDelay={2000}>
<Button
variant="ghost"
icon="arrow-right"
class="size-6 p-0"
disabled={!canForward()}
onClick={forward}
aria-label={language.t("common.goForward")}
/>
</div>
</Button>
</TooltipKeybind>
</Tooltip>
</div>
</div>
<div id="opencode-titlebar-left" class="flex items-center gap-3 min-w-0 px-2" data-tauri-drag-region />
<div class="flex-1 h-full" data-tauri-drag-region />
<div
+50 -21
View File
@@ -1,4 +1,4 @@
import { createEffect, createMemo, createSignal, onCleanup, onMount, type Accessor } from "solid-js"
import { createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js"
import { createStore } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useDialog } from "@opencode-ai/ui/context/dialog"
@@ -24,6 +24,15 @@ function normalizeKey(key: string) {
return key.toLowerCase()
}
function signature(key: string, ctrl: boolean, meta: boolean, shift: boolean, alt: boolean) {
const mask = (ctrl ? 1 : 0) | (meta ? 2 : 0) | (shift ? 4 : 0) | (alt ? 8 : 0)
return `${key}:${mask}`
}
function signatureFromEvent(event: KeyboardEvent) {
return signature(normalizeKey(event.key), event.ctrlKey, event.metaKey, event.shiftKey, event.altKey)
}
export type KeybindConfig = string
export interface Keybind {
@@ -156,8 +165,10 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
const dialog = useDialog()
const settings = useSettings()
const language = useLanguage()
const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([])
const [suspendCount, setSuspendCount] = createSignal(0)
const [store, setStore] = createStore({
registrations: [] as Accessor<CommandOption[]>[],
suspendCount: 0,
})
const [catalog, setCatalog, _, catalogReady] = persisted(
Persist.global("command.catalog.v1"),
@@ -175,7 +186,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
const seen = new Set<string>()
const all: CommandOption[] = []
for (const reg of registrations()) {
for (const reg of store.registrations) {
for (const opt of reg()) {
if (seen.has(opt.id)) continue
seen.add(opt.id)
@@ -221,7 +232,31 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
]
})
const suspended = () => suspendCount() > 0
const suspended = () => store.suspendCount > 0
const palette = createMemo(() => {
const config = settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND
const keybinds = parseKeybind(config)
return new Set(keybinds.map((kb) => signature(kb.key, kb.ctrl, kb.meta, kb.shift, kb.alt)))
})
const keymap = createMemo(() => {
const map = new Map<string, CommandOption>()
for (const option of options()) {
if (option.id.startsWith(SUGGESTED_PREFIX)) continue
if (option.disabled) continue
if (!option.keybind) continue
const keybinds = parseKeybind(option.keybind)
for (const kb of keybinds) {
if (!kb.key) continue
const sig = signature(kb.key, kb.ctrl, kb.meta, kb.shift, kb.alt)
if (map.has(sig)) continue
map.set(sig, option)
}
}
return map
})
const run = (id: string, source?: "palette" | "keybind" | "slash") => {
for (const option of options()) {
@@ -239,24 +274,18 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
const handleKeyDown = (event: KeyboardEvent) => {
if (suspended() || dialog.active) return
const paletteKeybinds = parseKeybind(settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND)
if (matchKeybind(paletteKeybinds, event)) {
const sig = signatureFromEvent(event)
if (palette().has(sig)) {
event.preventDefault()
showPalette()
return
}
for (const option of options()) {
if (option.disabled) continue
if (!option.keybind) continue
const keybinds = parseKeybind(option.keybind)
if (matchKeybind(keybinds, event)) {
event.preventDefault()
option.onSelect?.("keybind")
return
}
}
const option = keymap().get(sig)
if (!option) return
event.preventDefault()
option.onSelect?.("keybind")
}
onMount(() => {
@@ -270,9 +299,9 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
return {
register(cb: () => CommandOption[]) {
const results = createMemo(cb)
setRegistrations((arr) => [results, ...arr])
setStore("registrations", (arr) => [results, ...arr])
onCleanup(() => {
setRegistrations((arr) => arr.filter((x) => x !== results))
setStore("registrations", (arr) => arr.filter((x) => x !== results))
})
},
trigger(id: string, source?: "palette" | "keybind" | "slash") {
@@ -294,7 +323,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
},
show: showPalette,
keybinds(enabled: boolean) {
setSuspendCount((count) => count + (enabled ? -1 : 1))
setStore("suspendCount", (count) => count + (enabled ? -1 : 1))
},
suspended,
get catalog() {
+13 -5
View File
@@ -1,4 +1,4 @@
import { batch, createMemo, createRoot, createSignal, onCleanup } from "solid-js"
import { batch, createMemo, createRoot, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useParams } from "@solidjs/router"
@@ -37,8 +37,16 @@ function createCommentSession(dir: string, id: string | undefined) {
}),
)
const [focus, setFocus] = createSignal<CommentFocus | null>(null)
const [active, setActive] = createSignal<CommentFocus | null>(null)
const [state, setState] = createStore({
focus: null as CommentFocus | null,
active: null as CommentFocus | null,
})
const setFocus = (value: CommentFocus | null | ((value: CommentFocus | null) => CommentFocus | null)) =>
setState("focus", value)
const setActive = (value: CommentFocus | null | ((value: CommentFocus | null) => CommentFocus | null)) =>
setState("active", value)
const list = (file: string) => store.comments[file] ?? []
@@ -74,10 +82,10 @@ function createCommentSession(dir: string, id: string | undefined) {
all,
add,
remove,
focus: createMemo(() => focus()),
focus: createMemo(() => state.focus),
setFocus,
clearFocus: () => setFocus(null),
active: createMemo(() => active()),
active: createMemo(() => state.active),
setActive,
clearActive: () => setActive(null),
}
+327 -10
View File
@@ -1,7 +1,7 @@
import { createEffect, createMemo, createRoot, onCleanup } from "solid-js"
import { createStore, produce } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import type { FileContent } from "@opencode-ai/sdk/v2"
import type { FileContent, FileNode } from "@opencode-ai/sdk/v2"
import { showToast } from "@opencode-ai/ui/toast"
import { useParams } from "@solidjs/router"
import { getFilename } from "@opencode-ai/util/path"
@@ -39,6 +39,14 @@ export type FileState = {
content?: FileContent
}
type DirectoryState = {
expanded: boolean
loaded?: boolean
loading?: boolean
error?: string
children?: string[]
}
function stripFileProtocol(input: string) {
if (!input.startsWith("file://")) return input
return input.slice("file://".length)
@@ -57,6 +65,62 @@ function stripQueryAndHash(input: string) {
return input
}
function unquoteGitPath(input: string) {
if (!input.startsWith('"')) return input
if (!input.endsWith('"')) return input
const body = input.slice(1, -1)
const bytes: number[] = []
for (let i = 0; i < body.length; i++) {
const char = body[i]!
if (char !== "\\") {
bytes.push(char.charCodeAt(0))
continue
}
const next = body[i + 1]
if (!next) {
bytes.push("\\".charCodeAt(0))
continue
}
if (next >= "0" && next <= "7") {
const chunk = body.slice(i + 1, i + 4)
const match = chunk.match(/^[0-7]{1,3}/)
if (!match) {
bytes.push(next.charCodeAt(0))
i++
continue
}
bytes.push(parseInt(match[0], 8))
i += match[0].length
continue
}
const escaped =
next === "n"
? "\n"
: next === "r"
? "\r"
: next === "t"
? "\t"
: next === "b"
? "\b"
: next === "f"
? "\f"
: next === "v"
? "\v"
: next === "\\" || next === '"'
? next
: undefined
bytes.push((escaped ?? next).charCodeAt(0))
i++
}
return new TextDecoder().decode(new Uint8Array(bytes))
}
export function selectionFromLines(range: SelectedLineRange): FileSelection {
const startLine = Math.min(range.start, range.end)
const endLine = Math.max(range.start, range.end)
@@ -87,6 +151,28 @@ const WORKSPACE_KEY = "__workspace__"
const MAX_FILE_VIEW_SESSIONS = 20
const MAX_VIEW_FILES = 500
const MAX_FILE_CONTENT_ENTRIES = 40
const MAX_FILE_CONTENT_BYTES = 20 * 1024 * 1024
const contentLru = new Map<string, number>()
function approxBytes(content: FileContent) {
const patchBytes =
content.patch?.hunks.reduce((total, hunk) => {
return total + hunk.lines.reduce((sum, line) => sum + line.length, 0)
}, 0) ?? 0
return (content.content.length + (content.diff?.length ?? 0) + patchBytes) * 2
}
function touchContent(path: string, bytes?: number) {
const prev = contentLru.get(path)
if (prev === undefined && bytes === undefined) return
const value = bytes ?? prev ?? 0
contentLru.delete(path)
contentLru.set(path, value)
}
type ViewSession = ReturnType<typeof createViewSession>
type ViewCacheEntry = {
@@ -197,7 +283,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
const root = directory()
const prefix = root.endsWith("/") ? root : root + "/"
let path = stripQueryAndHash(stripFileProtocol(input))
let path = unquoteGitPath(stripQueryAndHash(stripFileProtocol(input)))
if (path.startsWith(prefix)) {
path = path.slice(prefix.length)
@@ -229,6 +315,13 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
}
const inflight = new Map<string, Promise<void>>()
const treeInflight = new Map<string, Promise<void>>()
const search = (query: string, dirs: "true" | "false") =>
sdk.client.find.files({ query, dirs }).then(
(x) => (x.data ?? []).map(normalize),
() => [],
)
const [store, setStore] = createStore<{
file: Record<string, FileState>
@@ -236,10 +329,51 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
file: {},
})
const [tree, setTree] = createStore<{
node: Record<string, FileNode>
dir: Record<string, DirectoryState>
}>({
node: {},
dir: { "": { expanded: true } },
})
const evictContent = (keep?: Set<string>) => {
const protectedSet = keep ?? new Set<string>()
const total = () => {
return Array.from(contentLru.values()).reduce((sum, bytes) => sum + bytes, 0)
}
while (contentLru.size > MAX_FILE_CONTENT_ENTRIES || total() > MAX_FILE_CONTENT_BYTES) {
const path = contentLru.keys().next().value
if (!path) return
if (protectedSet.has(path)) {
touchContent(path)
if (contentLru.size <= protectedSet.size) return
continue
}
contentLru.delete(path)
if (!store.file[path]) continue
setStore(
"file",
path,
produce((draft) => {
draft.content = undefined
draft.loaded = false
}),
)
}
}
createEffect(() => {
scope()
inflight.clear()
treeInflight.clear()
contentLru.clear()
setStore("file", {})
setTree("node", {})
setTree("dir", { "": { expanded: true } })
})
const viewCache = new Map<string, ViewCacheEntry>()
@@ -317,15 +451,20 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
.read({ path })
.then((x) => {
if (scope() !== directory) return
const content = x.data
setStore(
"file",
path,
produce((draft) => {
draft.loaded = true
draft.loading = false
draft.content = x.data
draft.content = content
}),
)
if (!content) return
touchContent(path, approxBytes(content))
evictContent(new Set([path]))
})
.catch((e) => {
if (scope() !== directory) return
@@ -351,17 +490,182 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
return promise
}
function normalizeDir(input: string) {
return normalize(input).replace(/\/+$/, "")
}
function ensureDir(path: string) {
if (tree.dir[path]) return
setTree("dir", path, { expanded: false })
}
function listDir(input: string, options?: { force?: boolean }) {
const dir = normalizeDir(input)
ensureDir(dir)
const current = tree.dir[dir]
if (!options?.force && current?.loaded) return Promise.resolve()
const pending = treeInflight.get(dir)
if (pending) return pending
setTree(
"dir",
dir,
produce((draft) => {
draft.loading = true
draft.error = undefined
}),
)
const directory = scope()
const promise = sdk.client.file
.list({ path: dir })
.then((x) => {
if (scope() !== directory) return
const nodes = x.data ?? []
const prevChildren = tree.dir[dir]?.children ?? []
const nextChildren = nodes.map((node) => node.path)
const nextSet = new Set(nextChildren)
setTree(
"node",
produce((draft) => {
const removedDirs: string[] = []
for (const child of prevChildren) {
if (nextSet.has(child)) continue
const existing = draft[child]
if (existing?.type === "directory") removedDirs.push(child)
delete draft[child]
}
if (removedDirs.length > 0) {
const keys = Object.keys(draft)
for (const key of keys) {
for (const removed of removedDirs) {
if (!key.startsWith(removed + "/")) continue
delete draft[key]
break
}
}
}
for (const node of nodes) {
draft[node.path] = node
}
}),
)
setTree(
"dir",
dir,
produce((draft) => {
draft.loaded = true
draft.loading = false
draft.children = nextChildren
}),
)
})
.catch((e) => {
if (scope() !== directory) return
setTree(
"dir",
dir,
produce((draft) => {
draft.loading = false
draft.error = e.message
}),
)
showToast({
variant: "error",
title: language.t("toast.file.listFailed.title"),
description: e.message,
})
})
.finally(() => {
treeInflight.delete(dir)
})
treeInflight.set(dir, promise)
return promise
}
function expandDir(input: string) {
const dir = normalizeDir(input)
ensureDir(dir)
setTree("dir", dir, "expanded", true)
void listDir(dir)
}
function collapseDir(input: string) {
const dir = normalizeDir(input)
ensureDir(dir)
setTree("dir", dir, "expanded", false)
}
function dirState(input: string) {
const dir = normalizeDir(input)
return tree.dir[dir]
}
function children(input: string) {
const dir = normalizeDir(input)
const ids = tree.dir[dir]?.children
if (!ids) return []
const out: FileNode[] = []
for (const id of ids) {
const node = tree.node[id]
if (node) out.push(node)
}
return out
}
const stop = sdk.event.listen((e) => {
const event = e.details
if (event.type !== "file.watcher.updated") return
const path = normalize(event.properties.file)
if (!path) return
if (path.startsWith(".git/")) return
if (!store.file[path]) return
load(path, { force: true })
if (store.file[path]) {
load(path, { force: true })
}
const kind = event.properties.event
if (kind === "change") {
const dir = (() => {
if (path === "") return ""
const node = tree.node[path]
if (node?.type !== "directory") return
return path
})()
if (dir === undefined) return
if (!tree.dir[dir]?.loaded) return
listDir(dir, { force: true })
return
}
if (kind !== "add" && kind !== "unlink") return
const parent = path.split("/").slice(0, -1).join("/")
if (!tree.dir[parent]?.loaded) return
listDir(parent, { force: true })
})
const get = (input: string) => store.file[normalize(input)]
const get = (input: string) => {
const path = normalize(input)
const file = store.file[path]
const content = file?.content
if (!content) return file
if (contentLru.has(path)) {
touchContent(path)
return file
}
touchContent(path, approxBytes(content))
return file
}
const scrollTop = (input: string) => view().scrollTop(normalize(input))
const scrollLeft = (input: string) => view().scrollLeft(normalize(input))
@@ -392,6 +696,21 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
normalize,
tab,
pathFromTab,
tree: {
list: listDir,
refresh: (input: string) => listDir(input, { force: true }),
state: dirState,
children,
expand: expandDir,
collapse: collapseDir,
toggle(input: string) {
if (dirState(input)?.expanded) {
collapseDir(input)
return
}
expandDir(input)
},
},
get,
load,
scrollTop,
@@ -400,10 +719,8 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
setScrollLeft,
selectedLines,
setSelectedLines,
searchFiles: (query: string) =>
sdk.client.find.files({ query, dirs: "false" }).then((x) => (x.data ?? []).map(normalize)),
searchFilesAndDirectories: (query: string) =>
sdk.client.find.files({ query, dirs: "true" }).then((x) => (x.data ?? []).map(normalize)),
searchFiles: (query: string) => search(query, "false"),
searchFilesAndDirectories: (query: string) => search(query, "true"),
}
},
})
+288 -85
View File
@@ -23,7 +23,7 @@ import { createStore, produce, reconcile, type SetStoreFunction, type Store } fr
import { Binary } from "@opencode-ai/util/binary"
import { retry } from "@opencode-ai/util/retry"
import { useGlobalSDK } from "./global-sdk"
import { ErrorPage, type InitError } from "../pages/error"
import type { InitError } from "../pages/error"
import {
batch,
createContext,
@@ -188,7 +188,74 @@ function createGlobalSync() {
config: {},
reload: undefined,
})
let bootstrapQueue: string[] = []
const queued = new Set<string>()
let root = false
let running = false
let timer: ReturnType<typeof setTimeout> | undefined
const paused = () => untrack(() => globalStore.reload) !== undefined
const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 0))
const take = (count: number) => {
if (queued.size === 0) return [] as string[]
const items: string[] = []
for (const item of queued) {
queued.delete(item)
items.push(item)
if (items.length >= count) break
}
return items
}
const schedule = () => {
if (timer) return
timer = setTimeout(() => {
timer = undefined
void drain()
}, 0)
}
const push = (directory: string) => {
if (!directory) return
queued.add(directory)
if (paused()) return
schedule()
}
const refresh = () => {
root = true
if (paused()) return
schedule()
}
async function drain() {
if (running) return
running = true
try {
while (true) {
if (paused()) return
if (root) {
root = false
await bootstrap()
await tick()
continue
}
const dirs = take(2)
if (dirs.length === 0) return
await Promise.all(dirs.map((dir) => bootstrapInstance(dir)))
await tick()
}
} finally {
running = false
if (paused()) return
if (root || queued.size) schedule()
}
}
createEffect(() => {
if (!projectCacheReady()) return
@@ -210,14 +277,8 @@ function createGlobalSync() {
createEffect(() => {
if (globalStore.reload !== "complete") return
if (bootstrapQueue.length) {
for (const directory of bootstrapQueue) {
bootstrapInstance(directory)
}
bootstrap()
}
bootstrapQueue = []
setGlobalStore("reload", undefined)
refresh()
})
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
@@ -225,6 +286,65 @@ function createGlobalSync() {
const sessionLoads = new Map<string, Promise<void>>()
const sessionMeta = new Map<string, { limit: number }>()
const sessionRecentWindow = 4 * 60 * 60 * 1000
const sessionRecentLimit = 50
function sessionUpdatedAt(session: Session) {
return session.time.updated ?? session.time.created
}
function compareSessionRecent(a: Session, b: Session) {
const aUpdated = sessionUpdatedAt(a)
const bUpdated = sessionUpdatedAt(b)
if (aUpdated !== bUpdated) return bUpdated - aUpdated
return a.id.localeCompare(b.id)
}
function takeRecentSessions(sessions: Session[], limit: number, cutoff: number) {
if (limit <= 0) return [] as Session[]
const selected: Session[] = []
const seen = new Set<string>()
for (const session of sessions) {
if (!session?.id) continue
if (seen.has(session.id)) continue
seen.add(session.id)
if (sessionUpdatedAt(session) <= cutoff) continue
const index = selected.findIndex((x) => compareSessionRecent(session, x) < 0)
if (index === -1) selected.push(session)
if (index !== -1) selected.splice(index, 0, session)
if (selected.length > limit) selected.pop()
}
return selected
}
function trimSessions(input: Session[], options: { limit: number; permission: Record<string, PermissionRequest[]> }) {
const limit = Math.max(0, options.limit)
const cutoff = Date.now() - sessionRecentWindow
const all = input
.filter((s) => !!s?.id)
.filter((s) => !s.time?.archived)
.sort((a, b) => a.id.localeCompare(b.id))
const roots = all.filter((s) => !s.parentID)
const children = all.filter((s) => !!s.parentID)
const base = roots.slice(0, limit)
const recent = takeRecentSessions(roots.slice(limit), sessionRecentLimit, cutoff)
const keepRoots = [...base, ...recent]
const keepRootIds = new Set(keepRoots.map((s) => s.id))
const keepChildren = children.filter((s) => {
if (s.parentID && keepRootIds.has(s.parentID)) return true
const perms = options.permission[s.id] ?? []
if (perms.length > 0) return true
return sessionUpdatedAt(s) > cutoff
})
return [...keepRoots, ...keepChildren].sort((a, b) => a.id.localeCompare(b.id))
}
function ensureChild(directory: string) {
if (!directory) console.error("No directory provided")
if (!children[directory]) {
@@ -323,7 +443,13 @@ function createGlobalSync() {
const [store, setStore] = child(directory, { bootstrap: false })
const meta = sessionMeta.get(directory)
if (meta && meta.limit >= store.limit) return
if (meta && meta.limit >= store.limit) {
const next = trimSessions(store.session, { limit: store.limit, permission: store.permission })
if (next.length !== store.session.length) {
setStore("session", reconcile(next, { key: "id" }))
}
return
}
const promise = globalSDK.client.session
.list({ directory, roots: true })
@@ -337,21 +463,9 @@ function createGlobalSync() {
// a request is in-flight still get the expanded result.
const limit = store.limit
const sandboxWorkspace = globalStore.project.some((p) => (p.sandboxes ?? []).includes(directory))
if (sandboxWorkspace) {
setStore("sessionTotal", nonArchived.length)
setStore("session", reconcile(nonArchived, { key: "id" }))
sessionMeta.set(directory, { limit })
return
}
const children = store.session.filter((s) => !!s.parentID)
const sessions = trimSessions([...nonArchived, ...children], { limit, permission: store.permission })
const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000
// Include up to the limit, plus any updated in the last 4 hours
const sessions = nonArchived.filter((s, i) => {
if (i < limit) return true
const updated = new Date(s.time?.updated ?? s.time?.created).getTime()
return updated > fourHoursAgo
})
// Store total session count (used for "load more" pagination)
setStore("sessionTotal", nonArchived.length)
setStore("session", reconcile(sessions, { key: "id" }))
@@ -493,6 +607,37 @@ function createGlobalSync() {
return promise
}
function purgeMessageParts(setStore: SetStoreFunction<State>, messageID: string | undefined) {
if (!messageID) return
setStore(
produce((draft) => {
delete draft.part[messageID]
}),
)
}
function purgeSessionData(store: Store<State>, setStore: SetStoreFunction<State>, sessionID: string | undefined) {
if (!sessionID) return
const messages = store.message[sessionID]
const messageIDs = (messages ?? []).map((m) => m.id).filter((id): id is string => !!id)
setStore(
produce((draft) => {
delete draft.message[sessionID]
delete draft.session_diff[sessionID]
delete draft.todo[sessionID]
delete draft.permission[sessionID]
delete draft.question[sessionID]
delete draft.session_status[sessionID]
for (const messageID of messageIDs) {
delete draft.part[messageID]
}
}),
)
}
const unsub = globalSDK.event.listen((e) => {
const directory = e.name
const event = e.details
@@ -500,9 +645,8 @@ function createGlobalSync() {
if (directory === "global") {
switch (event?.type) {
case "global.disposed": {
if (globalStore.reload) return
bootstrap()
break
refresh()
return
}
case "project.updated": {
const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id)
@@ -526,35 +670,66 @@ function createGlobalSync() {
if (!existing) return
const [store, setStore] = existing
const cleanupSessionCaches = (sessionID: string) => {
if (!sessionID) return
const hasAny =
store.message[sessionID] !== undefined ||
store.session_diff[sessionID] !== undefined ||
store.todo[sessionID] !== undefined ||
store.permission[sessionID] !== undefined ||
store.question[sessionID] !== undefined ||
store.session_status[sessionID] !== undefined
if (!hasAny) return
setStore(
produce((draft) => {
const messages = draft.message[sessionID]
if (messages) {
for (const message of messages) {
const id = message?.id
if (!id) continue
delete draft.part[id]
}
}
delete draft.message[sessionID]
delete draft.session_diff[sessionID]
delete draft.todo[sessionID]
delete draft.permission[sessionID]
delete draft.question[sessionID]
delete draft.session_status[sessionID]
}),
)
}
switch (event.type) {
case "server.instance.disposed": {
if (globalStore.reload) {
bootstrapQueue.push(directory)
return
}
bootstrapInstance(directory)
break
push(directory)
return
}
case "session.created": {
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
const info = event.properties.info
const result = Binary.search(store.session, info.id, (s) => s.id)
if (result.found) {
setStore("session", result.index, reconcile(event.properties.info))
setStore("session", result.index, reconcile(info))
break
}
setStore(
"session",
produce((draft) => {
draft.splice(result.index, 0, event.properties.info)
}),
)
if (!event.properties.info.parentID) {
setStore("sessionTotal", store.sessionTotal + 1)
const next = store.session.slice()
next.splice(result.index, 0, info)
const trimmed = trimSessions(next, { limit: store.limit, permission: store.permission })
setStore("session", reconcile(trimmed, { key: "id" }))
if (!info.parentID) {
setStore("sessionTotal", (value) => value + 1)
}
break
}
case "session.updated": {
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
if (event.properties.info.time.archived) {
const info = event.properties.info
const result = Binary.search(store.session, info.id, (s) => s.id)
if (info.time.archived) {
if (result.found) {
setStore(
"session",
@@ -563,24 +738,24 @@ function createGlobalSync() {
}),
)
}
if (event.properties.info.parentID) break
cleanupSessionCaches(info.id)
if (info.parentID) break
setStore("sessionTotal", (value) => Math.max(0, value - 1))
break
}
if (result.found) {
setStore("session", result.index, reconcile(event.properties.info))
setStore("session", result.index, reconcile(info))
break
}
setStore(
"session",
produce((draft) => {
draft.splice(result.index, 0, event.properties.info)
}),
)
const next = store.session.slice()
next.splice(result.index, 0, info)
const trimmed = trimSessions(next, { limit: store.limit, permission: store.permission })
setStore("session", reconcile(trimmed, { key: "id" }))
break
}
case "session.deleted": {
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
const sessionID = event.properties.info.id
const result = Binary.search(store.session, sessionID, (s) => s.id)
if (result.found) {
setStore(
"session",
@@ -589,6 +764,7 @@ function createGlobalSync() {
}),
)
}
cleanupSessionCaches(sessionID)
if (event.properties.info.parentID) break
setStore("sessionTotal", (value) => Math.max(0, value - 1))
break
@@ -624,18 +800,22 @@ function createGlobalSync() {
break
}
case "message.removed": {
const messages = store.message[event.properties.sessionID]
if (!messages) break
const result = Binary.search(messages, event.properties.messageID, (m) => m.id)
if (result.found) {
setStore(
"message",
event.properties.sessionID,
produce((draft) => {
draft.splice(result.index, 1)
}),
)
}
const sessionID = event.properties.sessionID
const messageID = event.properties.messageID
setStore(
produce((draft) => {
const messages = draft.message[sessionID]
if (messages) {
const result = Binary.search(messages, messageID, (m) => m.id)
if (result.found) {
messages.splice(result.index, 1)
}
}
delete draft.part[messageID]
}),
)
break
}
case "message.part.updated": {
@@ -660,15 +840,19 @@ function createGlobalSync() {
break
}
case "message.part.removed": {
const parts = store.part[event.properties.messageID]
const messageID = event.properties.messageID
const parts = store.part[messageID]
if (!parts) break
const result = Binary.search(parts, event.properties.partID, (p) => p.id)
if (result.found) {
setStore(
"part",
event.properties.messageID,
produce((draft) => {
draft.splice(result.index, 1)
const list = draft.part[messageID]
if (!list) return
const next = Binary.search(list, event.properties.partID, (p) => p.id)
if (!next.found) return
list.splice(next.index, 1)
if (list.length === 0) delete draft.part[messageID]
}),
)
}
@@ -765,6 +949,10 @@ function createGlobalSync() {
}
})
onCleanup(unsub)
onCleanup(() => {
if (!timer) return
clearTimeout(timer)
})
async function bootstrap() {
const health = await globalSDK.client.global
@@ -772,18 +960,23 @@ function createGlobalSync() {
.then((x) => x.data)
.catch(() => undefined)
if (!health?.healthy) {
setGlobalStore("error", new Error(language.t("error.globalSync.connectFailed", { url: globalSDK.url })))
showToast({
variant: "error",
title: language.t("dialog.server.add.error"),
description: language.t("error.globalSync.connectFailed", { url: globalSDK.url }),
})
setGlobalStore("ready", true)
return
}
return Promise.all([
const tasks = [
retry(() =>
globalSDK.client.path.get().then((x) => {
setGlobalStore("path", x.data!)
}),
),
retry(() =>
globalSDK.client.config.get().then((x) => {
globalSDK.client.global.config.get().then((x) => {
setGlobalStore("config", x.data!)
}),
),
@@ -807,9 +1000,22 @@ function createGlobalSync() {
setGlobalStore("provider_auth", x.data ?? {})
}),
),
])
.then(() => setGlobalStore("ready", true))
.catch((e) => setGlobalStore("error", e))
]
const results = await Promise.allSettled(tasks)
const errors = results.filter((r): r is PromiseRejectedResult => r.status === "rejected").map((r) => r.reason)
if (errors.length) {
const message = errors[0] instanceof Error ? errors[0].message : String(errors[0])
const more = errors.length > 1 ? ` (+${errors.length - 1} more)` : ""
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: message + more,
})
}
setGlobalStore("ready", true)
}
onMount(() => {
@@ -853,13 +1059,13 @@ function createGlobalSync() {
},
child,
bootstrap,
updateConfig: async (config: Config) => {
updateConfig: (config: Config) => {
setGlobalStore("reload", "pending")
const response = await globalSDK.client.config.update({ config })
setTimeout(() => {
setGlobalStore("reload", "complete")
}, 1000)
return response
return globalSDK.client.global.config.update({ config }).finally(() => {
setTimeout(() => {
setGlobalStore("reload", "complete")
}, 1000)
})
},
project: {
loadSessions,
@@ -875,9 +1081,6 @@ export function GlobalSyncProvider(props: ParentProps) {
const value = createGlobalSync()
return (
<Switch>
<Match when={value.error}>
<ErrorPage error={value.error} />
</Match>
<Match when={value.ready}>
<GlobalSyncContext.Provider value={value}>{props.children}</GlobalSyncContext.Provider>
</Match>
+225
View File
@@ -0,0 +1,225 @@
import { createEffect, createSignal, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { usePlatform } from "@/context/platform"
import { useSettings } from "@/context/settings"
import { persisted } from "@/utils/persist"
import { DialogReleaseNotes, type Highlight } from "@/components/dialog-release-notes"
const CHANGELOG_URL = "https://opencode.ai/changelog.json"
type Store = {
version?: string
}
type ParsedRelease = {
tag?: string
highlights: Highlight[]
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
}
function getText(value: unknown): string | undefined {
if (typeof value === "string") {
const text = value.trim()
return text.length > 0 ? text : undefined
}
if (typeof value === "number") return String(value)
return
}
function normalizeVersion(value: string | undefined) {
const text = value?.trim()
if (!text) return
return text.startsWith("v") || text.startsWith("V") ? text.slice(1) : text
}
function parseMedia(value: unknown, alt: string): Highlight["media"] | undefined {
if (!isRecord(value)) return
const type = getText(value.type)?.toLowerCase()
const src = getText(value.src) ?? getText(value.url)
if (!src) return
if (type !== "image" && type !== "video") return
return { type, src, alt }
}
function parseHighlight(value: unknown): Highlight | undefined {
if (!isRecord(value)) return
const title = getText(value.title)
if (!title) return
const description = getText(value.description) ?? getText(value.shortDescription)
if (!description) return
const media = parseMedia(value.media, title)
return { title, description, media }
}
function parseRelease(value: unknown): ParsedRelease | undefined {
if (!isRecord(value)) return
const tag = getText(value.tag) ?? getText(value.tag_name) ?? getText(value.name)
if (!Array.isArray(value.highlights)) {
return { tag, highlights: [] }
}
const highlights = value.highlights.flatMap((group) => {
if (!isRecord(group)) return []
const source = getText(group.source)
if (!source) return []
if (!source.toLowerCase().includes("desktop")) return []
if (Array.isArray(group.items)) {
return group.items.map((item) => parseHighlight(item)).filter((item): item is Highlight => item !== undefined)
}
const item = parseHighlight(group)
if (!item) return []
return [item]
})
return { tag, highlights }
}
function parseChangelog(value: unknown): ParsedRelease[] | undefined {
if (Array.isArray(value)) {
return value.map(parseRelease).filter((release): release is ParsedRelease => release !== undefined)
}
if (!isRecord(value)) return
if (!Array.isArray(value.releases)) return
return value.releases.map(parseRelease).filter((release): release is ParsedRelease => release !== undefined)
}
function sliceHighlights(input: { releases: ParsedRelease[]; current?: string; previous?: string }) {
const current = normalizeVersion(input.current)
const previous = normalizeVersion(input.previous)
const releases = input.releases
const start = (() => {
if (!current) return 0
const index = releases.findIndex((release) => normalizeVersion(release.tag) === current)
return index === -1 ? 0 : index
})()
const end = (() => {
if (!previous) return releases.length
const index = releases.findIndex((release, i) => i >= start && normalizeVersion(release.tag) === previous)
return index === -1 ? releases.length : index
})()
const highlights = releases.slice(start, end).flatMap((release) => release.highlights)
const seen = new Set<string>()
const unique = highlights.filter((highlight) => {
const key = [highlight.title, highlight.description, highlight.media?.type ?? "", highlight.media?.src ?? ""].join(
"\n",
)
if (seen.has(key)) return false
seen.add(key)
return true
})
return unique.slice(0, 5)
}
export const { use: useHighlights, provider: HighlightsProvider } = createSimpleContext({
name: "Highlights",
gate: false,
init: () => {
const platform = usePlatform()
const dialog = useDialog()
const settings = useSettings()
const [store, setStore, _, ready] = persisted("highlights.v1", createStore<Store>({ version: undefined }))
const [from, setFrom] = createSignal<string | undefined>(undefined)
const [to, setTo] = createSignal<string | undefined>(undefined)
const [timer, setTimer] = createSignal<ReturnType<typeof setTimeout> | undefined>(undefined)
const state = { started: false }
const markSeen = () => {
if (!platform.version) return
setStore("version", platform.version)
}
createEffect(() => {
if (state.started) return
if (!ready()) return
if (!settings.ready()) return
if (!platform.version) return
state.started = true
const previous = store.version
if (!previous) {
setStore("version", platform.version)
return
}
if (previous === platform.version) return
setFrom(previous)
setTo(platform.version)
if (!settings.general.releaseNotes()) {
markSeen()
return
}
const fetcher = platform.fetch ?? fetch
const controller = new AbortController()
onCleanup(() => {
controller.abort()
const id = timer()
if (id === undefined) return
clearTimeout(id)
})
fetcher(CHANGELOG_URL, {
signal: controller.signal,
headers: { Accept: "application/json" },
})
.then((response) => (response.ok ? (response.json() as Promise<unknown>) : undefined))
.then((json) => {
if (!json) return
const releases = parseChangelog(json)
if (!releases) return
if (releases.length === 0) return
const highlights = sliceHighlights({
releases,
current: platform.version,
previous,
})
if (controller.signal.aborted) return
if (highlights.length === 0) {
markSeen()
return
}
const timer = setTimeout(() => {
markSeen()
dialog.show(() => <DialogReleaseNotes highlights={highlights} />)
}, 500)
setTimer(timer)
})
.catch(() => undefined)
})
return {
ready,
from,
to,
get last() {
return store.version
},
markSeen,
}
},
})
+39 -2
View File
@@ -17,6 +17,7 @@ import { dict as ru } from "@/i18n/ru"
import { dict as ar } from "@/i18n/ar"
import { dict as no } from "@/i18n/no"
import { dict as br } from "@/i18n/br"
import { dict as th } from "@/i18n/th"
import { dict as uiEn } from "@opencode-ai/ui/i18n/en"
import { dict as uiZh } from "@opencode-ai/ui/i18n/zh"
import { dict as uiZht } from "@opencode-ai/ui/i18n/zht"
@@ -31,13 +32,45 @@ import { dict as uiRu } from "@opencode-ai/ui/i18n/ru"
import { dict as uiAr } from "@opencode-ai/ui/i18n/ar"
import { dict as uiNo } from "@opencode-ai/ui/i18n/no"
import { dict as uiBr } from "@opencode-ai/ui/i18n/br"
import { dict as uiTh } from "@opencode-ai/ui/i18n/th"
export type Locale = "en" | "zh" | "zht" | "ko" | "de" | "es" | "fr" | "da" | "ja" | "pl" | "ru" | "ar" | "no" | "br"
export type Locale =
| "en"
| "zh"
| "zht"
| "ko"
| "de"
| "es"
| "fr"
| "da"
| "ja"
| "pl"
| "ru"
| "ar"
| "no"
| "br"
| "th"
type RawDictionary = typeof en & typeof uiEn
type Dictionary = i18n.Flatten<RawDictionary>
const LOCALES: readonly Locale[] = ["en", "zh", "zht", "ko", "de", "es", "fr", "da", "ja", "pl", "ru", "ar", "no", "br"]
const LOCALES: readonly Locale[] = [
"en",
"zh",
"zht",
"ko",
"de",
"es",
"fr",
"da",
"ja",
"pl",
"ru",
"ar",
"no",
"br",
"th",
]
function detectLocale(): Locale {
if (typeof navigator !== "object") return "en"
@@ -65,6 +98,7 @@ function detectLocale(): Locale {
)
return "no"
if (language.toLowerCase().startsWith("pt")) return "br"
if (language.toLowerCase().startsWith("th")) return "th"
}
return "en"
@@ -94,6 +128,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
if (store.locale === "ar") return "ar"
if (store.locale === "no") return "no"
if (store.locale === "br") return "br"
if (store.locale === "th") return "th"
return "en"
})
@@ -118,6 +153,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
if (locale() === "ar") return { ...base, ...i18n.flatten({ ...ar, ...uiAr }) }
if (locale() === "no") return { ...base, ...i18n.flatten({ ...no, ...uiNo }) }
if (locale() === "br") return { ...base, ...i18n.flatten({ ...br, ...uiBr }) }
if (locale() === "th") return { ...base, ...i18n.flatten({ ...th, ...uiTh }) }
return { ...base, ...i18n.flatten({ ...ko, ...uiKo }) }
})
@@ -138,6 +174,7 @@ export const { use: useLanguage, provider: LanguageProvider } = createSimpleCont
ar: "language.ar",
no: "language.no",
br: "language.br",
th: "language.th",
}
const label = (value: Locale) => t(labelKey[value])
@@ -5,7 +5,7 @@ import { makePersisted, type SyncStorage } from "@solid-primitives/storage"
import { createScrollPersistence } from "./layout-scroll"
describe("createScrollPersistence", () => {
test("debounces persisted scroll writes", async () => {
test.skip("debounces persisted scroll writes", async () => {
const key = "layout-scroll.test"
const data = new Map<string, string>()
const writes: string[] = []
+80 -46
View File
@@ -51,16 +51,37 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const migrate = (value: unknown) => {
if (!isRecord(value)) return value
const sidebar = value.sidebar
if (!isRecord(sidebar)) return value
if (typeof sidebar.workspaces !== "boolean") return value
return {
...value,
sidebar: {
const migratedSidebar = (() => {
if (!isRecord(sidebar)) return sidebar
if (typeof sidebar.workspaces !== "boolean") return sidebar
return {
...sidebar,
workspaces: {},
workspacesDefault: sidebar.workspaces,
},
}
})()
const fileTree = value.fileTree
const migratedFileTree = (() => {
if (!isRecord(fileTree)) return fileTree
if (fileTree.tab === "changes" || fileTree.tab === "all") return fileTree
const width = typeof fileTree.width === "number" ? fileTree.width : 344
return {
...fileTree,
opened: true,
width: width === 260 ? 344 : width,
tab: "changes",
}
})()
if (migratedSidebar === sidebar && migratedFileTree === fileTree) return value
return {
...value,
sidebar: migratedSidebar,
fileTree: migratedFileTree,
}
}
@@ -80,7 +101,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
review: {
diffStyle: "split" as ReviewDiffStyle,
panelOpened: true,
},
fileTree: {
opened: true,
width: 344,
tab: "changes" as "changes" | "all",
},
session: {
width: 600,
@@ -218,7 +243,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}
function enrich(project: { worktree: string; expanded: boolean }) {
const [childStore] = globalSync.child(project.worktree)
const [childStore] = globalSync.child(project.worktree, { bootstrap: false })
const projectID = childStore.project
const metadata = projectID
? globalSync.data.project.find((x) => x.id === projectID)
@@ -449,6 +474,46 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setStore("review", "diffStyle", diffStyle)
},
},
fileTree: {
opened: createMemo(() => store.fileTree?.opened ?? true),
width: createMemo(() => store.fileTree?.width ?? 344),
tab: createMemo(() => store.fileTree?.tab ?? "changes"),
setTab(tab: "changes" | "all") {
if (!store.fileTree) {
setStore("fileTree", { opened: true, width: 344, tab })
return
}
setStore("fileTree", "tab", tab)
},
open() {
if (!store.fileTree) {
setStore("fileTree", { opened: true, width: 344, tab: "changes" })
return
}
setStore("fileTree", "opened", true)
},
close() {
if (!store.fileTree) {
setStore("fileTree", { opened: false, width: 344, tab: "changes" })
return
}
setStore("fileTree", "opened", false)
},
toggle() {
if (!store.fileTree) {
setStore("fileTree", { opened: true, width: 344, tab: "changes" })
return
}
setStore("fileTree", "opened", (x) => !x)
},
resize(width: number) {
if (!store.fileTree) {
setStore("fileTree", { opened: true, width, tab: "changes" })
return
}
setStore("fileTree", "width", width)
},
},
session: {
width: createMemo(() => store.session?.width ?? 600),
resize(width: number) {
@@ -490,7 +555,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const s = createMemo(() => store.sessionView[key()] ?? { scroll: {} })
const terminalOpened = createMemo(() => store.terminal?.opened ?? false)
const reviewPanelOpened = createMemo(() => store.review?.panelOpened ?? true)
function setTerminalOpened(next: boolean) {
const current = store.terminal
@@ -504,18 +568,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setStore("terminal", "opened", next)
}
function setReviewPanelOpened(next: boolean) {
const current = store.review
if (!current) {
setStore("review", { diffStyle: "split" as ReviewDiffStyle, panelOpened: next })
return
}
const value = current.panelOpened ?? true
if (value === next) return
setStore("review", "panelOpened", next)
}
return {
scroll(tab: string) {
return scroll.scroll(key(), tab)
@@ -535,18 +587,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setTerminalOpened(!terminalOpened())
},
},
reviewPanel: {
opened: reviewPanelOpened,
open() {
setReviewPanelOpened(true)
},
close() {
setReviewPanelOpened(false)
},
toggle() {
setReviewPanelOpened(!reviewPanelOpened())
},
},
review: {
open: createMemo(() => s().reviewOpen),
setOpen(open: string[]) {
@@ -584,10 +624,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const tabs = createMemo(() => store.sessionTabs[key()] ?? { all: [] })
return {
tabs,
active: createMemo(() => tabs().active),
all: createMemo(() => tabs().all),
active: createMemo(() => (tabs().active === "review" ? undefined : tabs().active)),
all: createMemo(() => tabs().all.filter((tab) => tab !== "review")),
setActive(tab: string | undefined) {
const session = key()
if (tab === "review") return
if (!store.sessionTabs[session]) {
setStore("sessionTabs", session, { all: [], active: tab })
} else {
@@ -596,25 +637,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
setAll(all: string[]) {
const session = key()
const next = all.filter((tab) => tab !== "review")
if (!store.sessionTabs[session]) {
setStore("sessionTabs", session, { all, active: undefined })
setStore("sessionTabs", session, { all: next, active: undefined })
} else {
setStore("sessionTabs", session, "all", all)
setStore("sessionTabs", session, "all", next)
}
},
async open(tab: string) {
if (tab === "review") return
const session = key()
const current = store.sessionTabs[session] ?? { all: [] }
if (tab === "review") {
if (!store.sessionTabs[session]) {
setStore("sessionTabs", session, { all: [], active: tab })
return
}
setStore("sessionTabs", session, "active", tab)
return
}
if (tab === "context") {
const all = [tab, ...current.all.filter((x) => x !== tab)]
if (!store.sessionTabs[session]) {
+15 -370
View File
@@ -1,49 +1,20 @@
import { createStore, produce, reconcile } from "solid-js/store"
import { batch, createEffect, createMemo, onCleanup } from "solid-js"
import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda"
import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk/v2"
import { createStore } from "solid-js/store"
import { batch, createMemo } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useSDK } from "./sdk"
import { useSync } from "./sync"
import { base64Encode } from "@opencode-ai/util/encode"
import { useProviders } from "@/hooks/use-providers"
import { DateTime } from "luxon"
import { Persist, persisted } from "@/utils/persist"
import { showToast } from "@opencode-ai/ui/toast"
import { useLanguage } from "@/context/language"
import { useModels } from "@/context/models"
export type LocalFile = FileNode &
Partial<{
loaded: boolean
pinned: boolean
expanded: boolean
content: FileContent
selection: { startLine: number; startChar: number; endLine: number; endChar: number }
scrollTop: number
view: "raw" | "diff-unified" | "diff-split"
folded: string[]
selectedChange: number
status: FileStatus
}>
export type TextSelection = LocalFile["selection"]
export type View = LocalFile["view"]
export type LocalModel = Omit<Model, "provider"> & {
provider: Provider
latest?: boolean
}
export type ModelKey = { providerID: string; modelID: string }
export type FileContext = { type: "file"; path: string; selection?: TextSelection }
export type ContextItem = FileContext
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
name: "Local",
init: () => {
const sdk = useSDK()
const sync = useSync()
const providers = useProviders()
const language = useLanguage()
function isModelValid(model: ModelKey) {
const provider = providers.all().find((x) => x.id === model.providerID)
@@ -112,18 +83,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})()
const model = (() => {
const [store, setStore, _, modelReady] = persisted(
Persist.global("model", ["model.v1"]),
createStore<{
user: (ModelKey & { visibility: "show" | "hide"; favorite?: boolean })[]
recent: ModelKey[]
variant?: Record<string, string | undefined>
}>({
user: [],
recent: [],
variant: {},
}),
)
const models = useModels()
const [ephemeral, setEphemeral] = createStore<{
model: Record<string, ModelKey | undefined>
@@ -131,57 +91,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
model: {},
})
const available = createMemo(() =>
providers.connected().flatMap((p) =>
Object.values(p.models).map((m) => ({
...m,
provider: p,
})),
),
)
const latest = createMemo(() =>
pipe(
available(),
filter((x) => Math.abs(DateTime.fromISO(x.release_date).diffNow().as("months")) < 6),
groupBy((x) => x.provider.id),
mapValues((models) =>
pipe(
models,
groupBy((x) => x.family),
values(),
(groups) =>
groups.flatMap((g) => {
const first = firstBy(g, [(x) => x.release_date, "desc"])
return first ? [{ modelID: first.id, providerID: first.provider.id }] : []
}),
),
),
values(),
flat(),
),
)
const latestSet = createMemo(() => new Set(latest().map((x) => `${x.providerID}:${x.modelID}`)))
const userVisibilityMap = createMemo(() => {
const map = new Map<string, "show" | "hide">()
for (const item of store.user) {
map.set(`${item.providerID}:${item.modelID}`, item.visibility)
}
return map
})
const list = createMemo(() =>
available().map((m) => ({
...m,
name: m.name.replace("(latest)", "").trim(),
latest: m.name.includes("(latest)"),
})),
)
const find = (key: ModelKey) => list().find((m) => m.id === key?.modelID && m.provider.id === key.providerID)
const fallbackModel = createMemo<ModelKey | undefined>(() => {
if (sync.data.config.model) {
const [providerID, modelID] = sync.data.config.model.split("/")
@@ -193,7 +102,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
}
for (const item of store.recent) {
for (const item of models.recent.list()) {
if (isModelValid(item)) {
return item
}
@@ -225,10 +134,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
fallbackModel,
)
if (!key) return undefined
return find(key)
return models.find(key)
})
const recent = createMemo(() => store.recent.map(find).filter(Boolean))
const recent = createMemo(() => models.recent.list().map(models.find).filter(Boolean))
const cycle = (direction: 1 | -1) => {
const recentList = recent()
@@ -253,54 +162,32 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})
}
function updateVisibility(model: ModelKey, visibility: "show" | "hide") {
const index = store.user.findIndex((x) => x.modelID === model.modelID && x.providerID === model.providerID)
if (index >= 0) {
setStore("user", index, { visibility })
} else {
setStore("user", store.user.length, { ...model, visibility })
}
}
return {
ready: modelReady,
ready: models.ready,
current,
recent,
list,
list: models.list,
cycle,
set(model: ModelKey | undefined, options?: { recent?: boolean }) {
batch(() => {
const currentAgent = agent.current()
const next = model ?? fallbackModel()
if (currentAgent) setEphemeral("model", currentAgent.name, next)
if (model) updateVisibility(model, "show")
if (options?.recent && model) {
const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID)
if (uniq.length > 5) uniq.pop()
setStore("recent", uniq)
}
if (model) models.setVisibility(model, true)
if (options?.recent && model) models.recent.push(model)
})
},
visible(model: ModelKey) {
const key = `${model.providerID}:${model.modelID}`
const visibility = userVisibilityMap().get(key)
if (visibility === "hide") return false
if (visibility === "show") return true
if (latestSet().has(key)) return true
// For models without valid release_date (e.g. custom models), show by default
const m = find(model)
if (!m?.release_date || !DateTime.fromISO(m.release_date).isValid) return true
return false
return models.visible(model)
},
setVisibility(model: ModelKey, visible: boolean) {
updateVisibility(model, visible ? "show" : "hide")
models.setVisibility(model, visible)
},
variant: {
current() {
const m = current()
if (!m) return undefined
const key = `${m.provider.id}/${m.id}`
return store.variant?.[key]
return models.variant.get({ providerID: m.provider.id, modelID: m.id })
},
list() {
const m = current()
@@ -311,12 +198,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
set(value: string | undefined) {
const m = current()
if (!m) return
const key = `${m.provider.id}/${m.id}`
if (!store.variant) {
setStore("variant", { [key]: value })
} else {
setStore("variant", key, value)
}
models.variant.set({ providerID: m.provider.id, modelID: m.id }, value)
},
cycle() {
const variants = this.list()
@@ -337,247 +219,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
})()
const file = (() => {
const [store, setStore] = createStore<{
node: Record<string, LocalFile>
}>({
node: {}, // Object.fromEntries(sync.data.node.map((x) => [x.path, x])),
})
const scope = createMemo(() => sdk.directory)
createEffect(() => {
scope()
setStore("node", {})
})
// const changeset = createMemo(() => new Set(sync.data.changes.map((f) => f.path)))
// const changes = createMemo(() => Array.from(changeset()).sort((a, b) => a.localeCompare(b)))
// createEffect((prev: FileStatus[]) => {
// const removed = prev.filter((p) => !sync.data.changes.find((c) => c.path === p.path))
// for (const p of removed) {
// setStore(
// "node",
// p.path,
// produce((draft) => {
// draft.status = undefined
// draft.view = "raw"
// }),
// )
// load(p.path)
// }
// for (const p of sync.data.changes) {
// if (store.node[p.path] === undefined) {
// fetch(p.path).then(() => {
// if (store.node[p.path] === undefined) return
// setStore("node", p.path, "status", p)
// })
// } else {
// setStore("node", p.path, "status", p)
// }
// }
// return sync.data.changes
// }, sync.data.changes)
// const changed = (path: string) => {
// const node = store.node[path]
// if (node?.status) return true
// const set = changeset()
// if (set.has(path)) return true
// for (const p of set) {
// if (p.startsWith(path ? path + "/" : "")) return true
// }
// return false
// }
// const resetNode = (path: string) => {
// setStore("node", path, {
// loaded: undefined,
// pinned: undefined,
// content: undefined,
// selection: undefined,
// scrollTop: undefined,
// folded: undefined,
// view: undefined,
// selectedChange: undefined,
// })
// }
const relative = (path: string) => path.replace(sync.data.path.directory + "/", "")
const load = async (path: string) => {
const directory = scope()
const client = sdk.client
const relativePath = relative(path)
await client.file
.read({ path: relativePath })
.then((x) => {
if (scope() !== directory) return
if (!store.node[relativePath]) return
setStore(
"node",
relativePath,
produce((draft) => {
draft.loaded = true
draft.content = x.data
}),
)
})
.catch((e) => {
if (scope() !== directory) return
showToast({
variant: "error",
title: language.t("toast.file.loadFailed.title"),
description: e.message,
})
})
}
const fetch = async (path: string) => {
const relativePath = relative(path)
const parent = relativePath.split("/").slice(0, -1).join("/")
if (parent) {
await list(parent)
}
}
const init = async (path: string) => {
const relativePath = relative(path)
if (!store.node[relativePath]) await fetch(path)
if (store.node[relativePath]?.loaded) return
return load(relativePath)
}
const open = async (path: string, options?: { pinned?: boolean; view?: LocalFile["view"] }) => {
const relativePath = relative(path)
if (!store.node[relativePath]) await fetch(path)
// setStore("opened", (x) => {
// if (x.includes(relativePath)) return x
// return [
// ...opened()
// .filter((x) => x.pinned)
// .map((x) => x.path),
// relativePath,
// ]
// })
// setStore("active", relativePath)
// context.addActive()
if (options?.pinned) setStore("node", path, "pinned", true)
if (options?.view && store.node[relativePath].view === undefined) setStore("node", path, "view", options.view)
if (store.node[relativePath]?.loaded) return
return load(relativePath)
}
const list = async (path: string) => {
const directory = scope()
const client = sdk.client
return client.file
.list({ path: path + "/" })
.then((x) => {
if (scope() !== directory) return
setStore(
"node",
produce((draft) => {
x.data!.forEach((node) => {
if (node.path in draft) return
draft[node.path] = node
})
}),
)
})
.catch(() => {})
}
const searchFiles = (query: string) => sdk.client.find.files({ query, dirs: "false" }).then((x) => x.data!)
const searchFilesAndDirectories = (query: string) =>
sdk.client.find.files({ query, dirs: "true" }).then((x) => x.data!)
const unsub = sdk.event.listen((e) => {
const event = e.details
switch (event.type) {
case "file.watcher.updated":
const relativePath = relative(event.properties.file)
if (relativePath.startsWith(".git/")) return
if (store.node[relativePath]) load(relativePath)
break
}
})
onCleanup(unsub)
return {
node: async (path: string) => {
if (!store.node[path] || !store.node[path].loaded) {
await init(path)
}
return store.node[path]
},
update: (path: string, node: LocalFile) => setStore("node", path, reconcile(node)),
open,
load,
init,
expand(path: string) {
setStore("node", path, "expanded", true)
if (store.node[path]?.loaded) return
setStore("node", path, "loaded", true)
list(path)
},
collapse(path: string) {
setStore("node", path, "expanded", false)
},
select(path: string, selection: TextSelection | undefined) {
setStore("node", path, "selection", selection)
},
scroll(path: string, scrollTop: number) {
setStore("node", path, "scrollTop", scrollTop)
},
view(path: string): View {
const n = store.node[path]
return n && n.view ? n.view : "raw"
},
setView(path: string, view: View) {
setStore("node", path, "view", view)
},
unfold(path: string, key: string) {
setStore("node", path, "folded", (xs) => {
const a = xs ?? []
if (a.includes(key)) return a
return [...a, key]
})
},
fold(path: string, key: string) {
setStore("node", path, "folded", (xs) => (xs ?? []).filter((k) => k !== key))
},
folded(path: string) {
const n = store.node[path]
return n && n.folded ? n.folded : []
},
changeIndex(path: string) {
return store.node[path]?.selectedChange
},
setChangeIndex(path: string, index: number | undefined) {
setStore("node", path, "selectedChange", index)
},
// changes,
// changed,
children(path: string) {
return Object.values(store.node).filter(
(x) =>
x.path.startsWith(path) &&
x.path !== path &&
!x.path.replace(new RegExp(`^${path + "/"}`), "").includes("/"),
)
},
searchFiles,
searchFilesAndDirectories,
relative,
}
})()
const result = {
slug: createMemo(() => base64Encode(sdk.directory)),
model,
agent,
file,
}
return result
},
+140
View File
@@ -0,0 +1,140 @@
import { createMemo } from "solid-js"
import { createStore } from "solid-js/store"
import { DateTime } from "luxon"
import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useProviders } from "@/hooks/use-providers"
import { Persist, persisted } from "@/utils/persist"
export type ModelKey = { providerID: string; modelID: string }
type Visibility = "show" | "hide"
type User = ModelKey & { visibility: Visibility; favorite?: boolean }
type Store = {
user: User[]
recent: ModelKey[]
variant?: Record<string, string | undefined>
}
export const { use: useModels, provider: ModelsProvider } = createSimpleContext({
name: "Models",
init: () => {
const providers = useProviders()
const [store, setStore, _, ready] = persisted(
Persist.global("model", ["model.v1"]),
createStore<Store>({
user: [],
recent: [],
variant: {},
}),
)
const available = createMemo(() =>
providers.connected().flatMap((p) =>
Object.values(p.models).map((m) => ({
...m,
provider: p,
})),
),
)
const latest = createMemo(() =>
pipe(
available(),
filter((x) => Math.abs(DateTime.fromISO(x.release_date).diffNow().as("months")) < 6),
groupBy((x) => x.provider.id),
mapValues((models) =>
pipe(
models,
groupBy((x) => x.family),
values(),
(groups) =>
groups.flatMap((g) => {
const first = firstBy(g, [(x) => x.release_date, "desc"])
return first ? [{ modelID: first.id, providerID: first.provider.id }] : []
}),
),
),
values(),
flat(),
),
)
const latestSet = createMemo(() => new Set(latest().map((x) => `${x.providerID}:${x.modelID}`)))
const visibility = createMemo(() => {
const map = new Map<string, Visibility>()
for (const item of store.user) map.set(`${item.providerID}:${item.modelID}`, item.visibility)
return map
})
const list = createMemo(() =>
available().map((m) => ({
...m,
name: m.name.replace("(latest)", "").trim(),
latest: m.name.includes("(latest)"),
})),
)
const find = (key: ModelKey) => list().find((m) => m.id === key.modelID && m.provider.id === key.providerID)
function update(model: ModelKey, state: Visibility) {
const index = store.user.findIndex((x) => x.modelID === model.modelID && x.providerID === model.providerID)
if (index >= 0) {
setStore("user", index, { visibility: state })
return
}
setStore("user", store.user.length, { ...model, visibility: state })
}
const visible = (model: ModelKey) => {
const key = `${model.providerID}:${model.modelID}`
const state = visibility().get(key)
if (state === "hide") return false
if (state === "show") return true
if (latestSet().has(key)) return true
const m = find(model)
if (!m?.release_date || !DateTime.fromISO(m.release_date).isValid) return true
return false
}
const setVisibility = (model: ModelKey, state: boolean) => {
update(model, state ? "show" : "hide")
}
const push = (model: ModelKey) => {
const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID)
if (uniq.length > 5) uniq.pop()
setStore("recent", uniq)
}
const variantKey = (model: ModelKey) => `${model.providerID}/${model.modelID}`
const getVariant = (model: ModelKey) => store.variant?.[variantKey(model)]
const setVariant = (model: ModelKey, value: string | undefined) => {
const key = variantKey(model)
if (!store.variant) {
setStore("variant", { [key]: value })
return
}
setStore("variant", key, value)
}
return {
ready,
list,
find,
visible,
setVisibility,
recent: {
list: createMemo(() => store.recent),
push,
},
variant: {
get: getVariant,
set: setVariant,
},
}
},
})
+66 -11
View File
@@ -1,5 +1,5 @@
import { createStore } from "solid-js/store"
import { createEffect, onCleanup } from "solid-js"
import { createEffect, createMemo, onCleanup } from "solid-js"
import { useParams } from "@solidjs/router"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSDK } from "./global-sdk"
@@ -8,7 +8,8 @@ import { usePlatform } from "@/context/platform"
import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings"
import { Binary } from "@opencode-ai/util/binary"
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
import { base64Encode } from "@opencode-ai/util/encode"
import { decode64 } from "@/utils/base64"
import { EventSessionError } from "@opencode-ai/sdk/v2"
import { Persist, persisted } from "@/utils/persist"
import { playSound, soundSrc } from "@/utils/sound"
@@ -52,6 +53,14 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
const settings = useSettings()
const language = useLanguage()
const empty: Notification[] = []
const currentDirectory = createMemo(() => {
return decode64(params.dir)
})
const currentSession = createMemo(() => params.id)
const [store, setStore, _, ready] = persisted(
Persist.global("notification", ["notification.v1"]),
createStore({
@@ -72,13 +81,59 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
setStore("list", (list) => pruneNotifications([...list, notification]))
}
const index = createMemo(() => {
const sessionAll = new Map<string, Notification[]>()
const sessionUnseen = new Map<string, Notification[]>()
const projectAll = new Map<string, Notification[]>()
const projectUnseen = new Map<string, Notification[]>()
for (const notification of store.list) {
const session = notification.session
if (session) {
const list = sessionAll.get(session)
if (list) list.push(notification)
else sessionAll.set(session, [notification])
if (!notification.viewed) {
const unseen = sessionUnseen.get(session)
if (unseen) unseen.push(notification)
else sessionUnseen.set(session, [notification])
}
}
const directory = notification.directory
if (directory) {
const list = projectAll.get(directory)
if (list) list.push(notification)
else projectAll.set(directory, [notification])
if (!notification.viewed) {
const unseen = projectUnseen.get(directory)
if (unseen) unseen.push(notification)
else projectUnseen.set(directory, [notification])
}
}
}
return {
session: {
all: sessionAll,
unseen: sessionUnseen,
},
project: {
all: projectAll,
unseen: projectUnseen,
},
}
})
const unsub = globalSDK.event.listen((e) => {
const directory = e.name
const event = e.details
if (event.type !== "session.idle" && event.type !== "session.error") return
const directory = e.name
const time = Date.now()
const activeDirectory = params.dir ? base64Decode(params.dir) : undefined
const activeSession = params.id
const viewed = (sessionID?: string) => {
const activeDirectory = currentDirectory()
const activeSession = currentSession()
if (!activeDirectory) return false
if (!activeSession) return false
if (!sessionID) return false
@@ -88,7 +143,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
switch (event.type) {
case "session.idle": {
const sessionID = event.properties.sessionID
const [syncStore] = globalSync.child(directory)
const [syncStore] = globalSync.child(directory, { bootstrap: false })
const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
const session = match.found ? syncStore.session[match.index] : undefined
if (session?.parentID) break
@@ -115,7 +170,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
}
case "session.error": {
const sessionID = event.properties.sessionID
const [syncStore] = globalSync.child(directory)
const [syncStore] = globalSync.child(directory, { bootstrap: false })
const match = sessionID ? Binary.search(syncStore.session, sessionID, (s) => s.id) : undefined
const session = sessionID && match?.found ? syncStore.session[match.index] : undefined
if (session?.parentID) break
@@ -148,10 +203,10 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
ready,
session: {
all(session: string) {
return store.list.filter((n) => n.session === session)
return index().session.all.get(session) ?? empty
},
unseen(session: string) {
return store.list.filter((n) => n.session === session && !n.viewed)
return index().session.unseen.get(session) ?? empty
},
markViewed(session: string) {
setStore("list", (n) => n.session === session, "viewed", true)
@@ -159,10 +214,10 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
},
project: {
all(directory: string) {
return store.list.filter((n) => n.directory === directory)
return index().project.all.get(directory) ?? empty
},
unseen(directory: string) {
return store.list.filter((n) => n.directory === directory && !n.viewed)
return index().project.unseen.get(directory) ?? empty
},
markViewed(directory: string) {
setStore("list", (n) => n.directory === directory, "viewed", true)
+24 -5
View File
@@ -6,7 +6,8 @@ import { Persist, persisted } from "@/utils/persist"
import { useGlobalSDK } from "@/context/global-sdk"
import { useGlobalSync } from "./global-sync"
import { useParams } from "@solidjs/router"
import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
import { base64Encode } from "@opencode-ai/util/encode"
import { decode64 } from "@/utils/base64"
type PermissionRespondFn = (input: {
sessionID: string
@@ -53,7 +54,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
const globalSync = useGlobalSync()
const permissionsEnabled = createMemo(() => {
const directory = params.dir ? base64Decode(params.dir) : undefined
const directory = decode64(params.dir)
if (!directory) return false
const [store] = globalSync.child(directory)
return hasAutoAcceptPermissionConfig(store.config.permission)
@@ -66,7 +67,21 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
}),
)
const responded = new Set<string>()
const MAX_RESPONDED = 1000
const RESPONDED_TTL_MS = 60 * 60 * 1000
const responded = new Map<string, number>()
function pruneResponded(now: number) {
for (const [id, ts] of responded) {
if (now - ts < RESPONDED_TTL_MS) break
responded.delete(id)
}
for (const id of responded.keys()) {
if (responded.size <= MAX_RESPONDED) break
responded.delete(id)
}
}
const respond: PermissionRespondFn = (input) => {
globalSDK.client.permission.respond(input).catch(() => {
@@ -75,8 +90,12 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
}
function respondOnce(permission: PermissionRequest, directory?: string) {
if (responded.has(permission.id)) return
responded.add(permission.id)
const now = Date.now()
const hit = responded.has(permission.id)
responded.delete(permission.id)
responded.set(permission.id, now)
pruneResponded(now)
if (hit) return
respond({
sessionID: permission.sessionID,
permissionID: permission.id,
+10 -4
View File
@@ -17,6 +17,12 @@ export type Platform = {
/** Restart the app */
restart(): Promise<void>
/** Navigate back in history */
back(): void
/** Navigate forward in history */
forward(): void
/** Send a system notification (optional deep link) */
notify(title: string, description?: string, href?: string): Promise<void>
@@ -41,11 +47,11 @@ export type Platform = {
/** Fetch override */
fetch?: typeof fetch
/** Get the configured default server URL (desktop only) */
getDefaultServerUrl?(): Promise<string | null>
/** Get the configured default server URL (platform-specific) */
getDefaultServerUrl?(): Promise<string | null> | string | null
/** Set the default server URL to use on app startup (desktop only) */
setDefaultServerUrl?(url: string | null): Promise<void>
/** Set the default server URL to use on app startup (platform-specific) */
setDefaultServerUrl?(url: string | null): Promise<void> | void
/** Parse markdown to HTML using native parser (desktop only, returns unprocessed code blocks) */
parseMarkdown?(markdown: string): Promise<string>
+23 -19
View File
@@ -1,6 +1,6 @@
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { batch, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
import { batch, createEffect, createMemo, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { usePlatform } from "@/context/platform"
import { Persist, persisted } from "@/utils/persist"
@@ -40,12 +40,17 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
}),
)
const [active, setActiveRaw] = createSignal("")
const [state, setState] = createStore({
active: "",
healthy: undefined as boolean | undefined,
})
const healthy = () => state.healthy
function setActive(input: string) {
const url = normalizeServerUrl(input)
if (!url) return
setActiveRaw(url)
setState("active", url)
}
function add(input: string) {
@@ -54,7 +59,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
const fallback = normalizeServerUrl(props.defaultUrl)
if (fallback && url === fallback) {
setActiveRaw(url)
setState("active", url)
return
}
@@ -62,7 +67,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
if (!store.list.includes(url)) {
setStore("list", store.list.length, url)
}
setActiveRaw(url)
setState("active", url)
})
}
@@ -71,31 +76,30 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
if (!url) return
const list = store.list.filter((x) => x !== url)
const next = active() === url ? (list[0] ?? normalizeServerUrl(props.defaultUrl) ?? "") : active()
const next = state.active === url ? (list[0] ?? normalizeServerUrl(props.defaultUrl) ?? "") : state.active
batch(() => {
setStore("list", list)
setActiveRaw(next)
setState("active", next)
})
}
createEffect(() => {
if (!ready()) return
if (active()) return
if (state.active) return
const url = normalizeServerUrl(props.defaultUrl)
if (!url) return
setActiveRaw(url)
setState("active", url)
})
const isReady = createMemo(() => ready() && !!active())
const [healthy, setHealthy] = createSignal<boolean | undefined>(undefined)
const isReady = createMemo(() => ready() && !!state.active)
const check = (url: string) => {
const signal = (AbortSignal as unknown as { timeout?: (ms: number) => AbortSignal }).timeout?.(3000)
const sdk = createOpencodeClient({
baseUrl: url,
fetch: platform.fetch,
signal: AbortSignal.timeout(3000),
signal,
})
return sdk.global
.health()
@@ -104,10 +108,10 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
}
createEffect(() => {
const url = active()
const url = state.active
if (!url) return
setHealthy(undefined)
setState("healthy", undefined)
let alive = true
let busy = false
@@ -118,7 +122,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
void check(url)
.then((next) => {
if (!alive) return
setHealthy(next)
setState("healthy", next)
})
.finally(() => {
busy = false
@@ -134,7 +138,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
})
})
const origin = createMemo(() => projectsKey(active()))
const origin = createMemo(() => projectsKey(state.active))
const projectsList = createMemo(() => store.projects[origin()] ?? [])
const isLocal = createMemo(() => origin() === "local")
@@ -143,10 +147,10 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
healthy,
isLocal,
get url() {
return active()
return state.active
},
get name() {
return serverDisplayName(active())
return serverDisplayName(state.active)
},
get list() {
return store.list
+18
View File
@@ -18,6 +18,10 @@ export interface SoundSettings {
export interface Settings {
general: {
autoSave: boolean
releaseNotes: boolean
}
updates: {
startup: boolean
}
appearance: {
fontSize: number
@@ -34,6 +38,10 @@ export interface Settings {
const defaultSettings: Settings = {
general: {
autoSave: true,
releaseNotes: true,
},
updates: {
startup: true,
},
appearance: {
fontSize: 14,
@@ -97,6 +105,16 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
setAutoSave(value: boolean) {
setStore("general", "autoSave", value)
},
releaseNotes: createMemo(() => store.general?.releaseNotes ?? defaultSettings.general.releaseNotes),
setReleaseNotes(value: boolean) {
setStore("general", "releaseNotes", value)
},
},
updates: {
startup: createMemo(() => store.updates?.startup ?? defaultSettings.updates.startup),
setStartup(value: boolean) {
setStore("updates", "startup", value)
},
},
appearance: {
fontSize: createMemo(() => store.appearance?.fontSize ?? defaultSettings.appearance.fontSize),
+15 -28
View File
@@ -16,7 +16,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const sdk = useSDK()
type Child = ReturnType<(typeof globalSync)["child"]>
type Store = Child[0]
type Setter = Child[1]
const current = createMemo(() => globalSync.child(sdk.directory))
@@ -43,18 +42,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
return Math.ceil(count / chunk) * chunk
}
const hydrateMessages = (directory: string, store: Store, sessionID: string) => {
const key = keyFor(directory, sessionID)
if (meta.limit[key] !== undefined) return
const messages = store.message[sessionID]
if (!messages) return
const limit = limitFor(messages.length)
setMeta("limit", key, limit)
setMeta("complete", key, messages.length < limit)
}
const loadMessages = async (input: {
directory: string
client: typeof sdk.client
@@ -150,21 +137,20 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const directory = sdk.directory
const client = sdk.client
const [store, setStore] = globalSync.child(directory)
const key = keyFor(directory, sessionID)
const hasSession = (() => {
const match = Binary.search(store.session, sessionID, (s) => s.id)
return match.found
})()
hydrateMessages(directory, store, sessionID)
const hasMessages = store.message[sessionID] !== undefined
if (hasSession && hasMessages) return
const key = keyFor(directory, sessionID)
const hydrated = meta.limit[key] !== undefined
if (hasSession && hasMessages && hydrated) return
const pending = inflight.get(key)
if (pending) return pending
const limit = meta.limit[key] ?? chunk
const count = store.message[sessionID]?.length ?? 0
const limit = hydrated ? (meta.limit[key] ?? chunk) : limitFor(count)
const sessionReq = hasSession
? Promise.resolve()
@@ -184,15 +170,16 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
)
})
const messagesReq = hasMessages
? Promise.resolve()
: loadMessages({
directory,
client,
setStore,
sessionID,
limit,
})
const messagesReq =
hasMessages && hydrated
? Promise.resolve()
: loadMessages({
directory,
client,
setStore,
sessionID,
limit,
})
const promise = Promise.all([sessionReq, messagesReq])
.then(() => {})
+3 -2
View File
@@ -155,8 +155,9 @@ function createTerminalSession(sdk: ReturnType<typeof useSDK>, dir: string, sess
batch(() => {
setStore("all", index, {
...pty,
...clone.data,
id: clone.data.id,
title: clone.data.title ?? pty.title,
titleNumber: pty.titleNumber,
})
if (active) {
setStore("active", clone.data.id)
+28
View File
@@ -6,6 +6,8 @@ import { dict as en } from "@/i18n/en"
import { dict as zh } from "@/i18n/zh"
import pkg from "../package.json"
const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl"
const root = document.getElementById("root")
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
const locale = (() => {
@@ -29,6 +31,12 @@ const platform: Platform = {
openLink(url: string) {
window.open(url, "_blank")
},
back() {
window.history.back()
},
forward() {
window.history.forward()
},
restart: async () => {
window.location.reload()
},
@@ -62,6 +70,26 @@ const platform: Platform = {
})
.catch(() => undefined)
},
getDefaultServerUrl: () => {
if (typeof localStorage === "undefined") return null
try {
return localStorage.getItem(DEFAULT_SERVER_URL_KEY)
} catch {
return null
}
},
setDefaultServerUrl: (url) => {
if (typeof localStorage === "undefined") return
try {
if (url) {
localStorage.setItem(DEFAULT_SERVER_URL_KEY, url)
return
}
localStorage.removeItem(DEFAULT_SERVER_URL_KEY)
} catch {
return
}
},
}
render(
+2 -2
View File
@@ -1,5 +1,5 @@
import { useGlobalSync } from "@/context/global-sync"
import { base64Decode } from "@opencode-ai/util/encode"
import { decode64 } from "@/utils/base64"
import { useParams } from "@solidjs/router"
import { createMemo } from "solid-js"
@@ -8,7 +8,7 @@ export const popularProviders = ["opencode", "anthropic", "github-copilot", "ope
export function useProviders() {
const globalSync = useGlobalSync()
const params = useParams()
const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
const currentDirectory = createMemo(() => decode64(params.dir) ?? "")
const providers = createMemo(() => {
if (currentDirectory()) {
const [projectStore] = globalSync.child(currentDirectory())
+68 -14
View File
@@ -8,6 +8,7 @@ export const dict = {
"command.category.theme": "سمة",
"command.category.language": "لغة",
"command.category.file": "ملف",
"command.category.context": "سياق",
"command.category.terminal": "محطة طرفية",
"command.category.model": "نموذج",
"command.category.mcp": "MCP",
@@ -42,7 +43,10 @@ export const dict = {
"command.session.new": "جلسة جديدة",
"command.file.open": "فتح ملف",
"command.file.open.description": "البحث في الملفات والأوامر",
"command.context.addSelection": "إضافة التحديد إلى السياق",
"command.context.addSelection.description": "إضافة الأسطر المحددة من الملف الحالي",
"command.terminal.toggle": "تبديل المحطة الطرفية",
"command.fileTree.toggle": "تبديل شجرة الملفات",
"command.review.toggle": "تبديل المراجعة",
"command.terminal.new": "محطة طرفية جديدة",
"command.terminal.new.description": "إنشاء علامة تبويب جديدة للمحطة الطرفية",
@@ -100,7 +104,7 @@ export const dict = {
"dialog.model.unpaid.freeModels.title": "نماذج مجانية مقدمة من OpenCode",
"dialog.model.unpaid.addMore.title": "إضافة المزيد من النماذج من موفرين مشهورين",
"dialog.provider.viewAll": "عرض جميع الموفرين",
"dialog.provider.viewAll": "عرض المزيد من الموفرين",
"provider.connect.title": "اتصال {{provider}}",
"provider.connect.title.anthropicProMax": "تسجيل الدخول باستخدام Claude Pro/Max",
@@ -137,6 +141,8 @@ export const dict = {
"provider.connect.toast.connected.title": "تم توصيل {{provider}}",
"provider.connect.toast.connected.description": "نماذج {{provider}} متاحة الآن للاستخدام.",
"provider.disconnect.toast.disconnected.title": "تم فصل {{provider}}",
"provider.disconnect.toast.disconnected.description": "لم تعد نماذج {{provider}} متاحة.",
"model.tag.free": "مجاني",
"model.tag.latest": "الأحدث",
"model.provider.anthropic": "Anthropic",
@@ -159,6 +165,8 @@ export const dict = {
"common.loading": "جارٍ التحميل",
"common.loading.ellipsis": "...",
"common.cancel": "إلغاء",
"common.connect": "اتصال",
"common.disconnect": "قطع الاتصال",
"common.submit": "إرسال",
"common.save": "حفظ",
"common.saving": "جارٍ الحفظ...",
@@ -167,6 +175,8 @@ export const dict = {
"prompt.placeholder.shell": "أدخل أمر shell...",
"prompt.placeholder.normal": 'اسأل أي شيء... "{{example}}"',
"prompt.placeholder.summarizeComments": "لخّص التعليقات…",
"prompt.placeholder.summarizeComment": "لخّص التعليق…",
"prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "esc للخروج",
@@ -270,6 +280,9 @@ export const dict = {
"dialog.project.edit.color": "لون",
"dialog.project.edit.color.select": "اختر لون {{color}}",
"dialog.project.edit.worktree.startup": "سكريبت بدء تشغيل مساحة العمل",
"dialog.project.edit.worktree.startup.description": "يتم تشغيله بعد إنشاء مساحة عمل جديدة (شجرة عمل).",
"dialog.project.edit.worktree.startup.placeholder": "مثال: bun install",
"context.breakdown.title": "تفصيل السياق",
"context.breakdown.note": 'تفصيل تقريبي لرموز الإدخال. يشمل "أخرى" تعريفات الأدوات والنفقات العامة.',
"context.breakdown.system": "النظام",
@@ -304,20 +317,21 @@ export const dict = {
"context.usage.clickToView": "انقر لعرض السياق",
"context.usage.view": "عرض استخدام السياق",
"language.en": "الإنجليزية",
"language.zh": "الصينية (المبسطة)",
"language.zht": "الصينية (التقليدية)",
"language.ko": "الكورية",
"language.de": "الألمانية",
"language.es": "الإسبانية",
"language.fr": "الفرنسية",
"language.ja": "اليابانية",
"language.da": "الدانماركية",
"language.ru": "الروسية",
"language.pl": "البولندية",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "النرويجية",
"language.br": "البرتغالية (البرازيل)",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "لغة",
"toast.language.description": "تم التبديل إلى {{language}}",
@@ -335,6 +349,9 @@ export const dict = {
"toast.file.loadFailed.title": "فشل تحميل الملف",
"toast.file.listFailed.title": "فشل سرد الملفات",
"toast.context.noLineSelection.title": "لا يوجد تحديد للأسطر",
"toast.context.noLineSelection.description": "حدد نطاق أسطر في تبويب ملف أولاً.",
"toast.session.share.copyFailed.title": "فشل نسخ عنوان URL إلى الحافظة",
"toast.session.share.success.title": "تمت مشاركة الجلسة",
"toast.session.share.success.description": "تم نسخ عنوان URL للمشاركة إلى الحافظة!",
@@ -408,8 +425,13 @@ export const dict = {
"session.tab.context": "سياق",
"session.panel.reviewAndFiles": "المراجعة والملفات",
"session.review.filesChanged": "تم تغيير {{count}} ملفات",
"session.review.change.one": "تغيير",
"session.review.change.other": "تغييرات",
"session.review.loadingChanges": "جارٍ تحميل التغييرات...",
"session.review.empty": "لا توجد تغييرات في هذه الجلسة بعد",
"session.review.noChanges": "لا توجد تغييرات",
"session.files.selectToOpen": "اختر ملفًا لفتحه",
"session.files.all": "كل الملفات",
"session.messages.renderEarlier": "عرض الرسائل السابقة",
"session.messages.loadingEarlier": "جارٍ تحميل الرسائل السابقة...",
"session.messages.loadEarlier": "تحميل الرسائل السابقة",
@@ -426,6 +448,14 @@ export const dict = {
"session.header.search.placeholder": "بحث {{project}}",
"session.header.searchFiles": "بحث عن الملفات",
"status.popover.trigger": "الحالة",
"status.popover.ariaLabel": "إعدادات الخوادم",
"status.popover.tab.servers": "الخوادم",
"status.popover.tab.mcp": "MCP",
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "الإضافات",
"status.popover.action.manageServers": "إدارة الخوادم",
"session.share.popover.title": "نشر على الويب",
"session.share.popover.description.shared": "هذه الجلسة عامة على الويب. يمكن لأي شخص لديه الرابط الوصول إليها.",
"session.share.popover.description.unshared": "شارك الجلسة علنًا على الويب. ستكون متاحة لأي شخص لديه الرابط.",
@@ -475,12 +505,15 @@ export const dict = {
"sidebar.project.recentSessions": "الجلسات الحديثة",
"sidebar.project.viewAllSessions": "عرض جميع الجلسات",
"app.name.desktop": "OpenCode Desktop",
"settings.section.desktop": "سطح المكتب",
"settings.section.server": "الخادم",
"settings.tab.general": "عام",
"settings.tab.shortcuts": "اختصارات",
"settings.general.section.appearance": "المظهر",
"settings.general.section.notifications": "إشعارات النظام",
"settings.general.section.updates": "التحديثات",
"settings.general.section.sounds": "المؤثرات الصوتية",
"settings.general.row.language.title": "اللغة",
@@ -491,12 +524,25 @@ export const dict = {
"settings.general.row.theme.description": "تخصيص سمة OpenCode.",
"settings.general.row.font.title": "الخط",
"settings.general.row.font.description": "تخصيص الخط الأحادي المستخدم في كتل التعليمات البرمجية",
"settings.general.row.releaseNotes.title": "ملاحظات الإصدار",
"settings.general.row.releaseNotes.description": 'عرض نوافذ "ما الجديد" المنبثقة بعد التحديثات',
"settings.updates.row.startup.title": "التحقق من التحديثات عند بدء التشغيل",
"settings.updates.row.startup.description": "التحقق تلقائيًا من التحديثات عند تشغيل OpenCode",
"settings.updates.row.check.title": "التحقق من التحديثات",
"settings.updates.row.check.description": "التحقق يدويًا من التحديثات وتثبيتها إذا كانت متاحة",
"settings.updates.action.checkNow": "تحقق الآن",
"settings.updates.action.checking": "جارٍ التحقق...",
"settings.updates.toast.latest.title": "أنت على آخر إصدار",
"settings.updates.toast.latest.description": "أنت تستخدم أحدث إصدار من OpenCode.",
"font.option.ibmPlexMono": "IBM Plex Mono",
"font.option.cascadiaCode": "Cascadia Code",
"font.option.firaCode": "Fira Code",
"font.option.hack": "Hack",
"font.option.inconsolata": "Inconsolata",
"font.option.intelOneMono": "Intel One Mono",
"font.option.iosevka": "Iosevka",
"font.option.jetbrainsMono": "JetBrains Mono",
"font.option.mesloLgs": "Meslo LGS",
"font.option.robotoMono": "Roboto Mono",
@@ -582,6 +628,13 @@ export const dict = {
"settings.providers.title": "الموفرون",
"settings.providers.description": "ستكون إعدادات الموفر قابلة للتكوين هنا.",
"settings.providers.section.connected": "الموفرون المتصلون",
"settings.providers.connected.empty": "لا يوجد موفرون متصلون",
"settings.providers.section.popular": "الموفرون الشائعون",
"settings.providers.tag.environment": "البيئة",
"settings.providers.tag.config": "التكوين",
"settings.providers.tag.custom": "مخصص",
"settings.providers.tag.other": "أخرى",
"settings.models.title": "النماذج",
"settings.models.description": "ستكون إعدادات النموذج قابلة للتكوين هنا.",
"settings.agents.title": "الوكلاء",
@@ -649,6 +702,7 @@ export const dict = {
"workspace.reset.failed.title": "فشل إعادة تعيين مساحة العمل",
"workspace.reset.success.title": "تمت إعادة تعيين مساحة العمل",
"workspace.reset.success.description": "مساحة العمل تطابق الآن الفرع الافتراضي.",
"workspace.error.stillPreparing": "مساحة العمل لا تزال قيد الإعداد",
"workspace.status.checking": "التحقق من التغييرات غير المدمجة...",
"workspace.status.error": "تعذر التحقق من حالة git.",
"workspace.status.clean": "لم يتم اكتشاف تغييرات غير مدمجة.",
+73 -14
View File
@@ -8,6 +8,7 @@ export const dict = {
"command.category.theme": "Tema",
"command.category.language": "Idioma",
"command.category.file": "Arquivo",
"command.category.context": "Contexto",
"command.category.terminal": "Terminal",
"command.category.model": "Modelo",
"command.category.mcp": "MCP",
@@ -42,7 +43,10 @@ export const dict = {
"command.session.new": "Nova sessão",
"command.file.open": "Abrir arquivo",
"command.file.open.description": "Buscar arquivos e comandos",
"command.context.addSelection": "Adicionar seleção ao contexto",
"command.context.addSelection.description": "Adicionar as linhas selecionadas do arquivo atual",
"command.terminal.toggle": "Alternar terminal",
"command.fileTree.toggle": "Alternar árvore de arquivos",
"command.review.toggle": "Alternar revisão",
"command.terminal.new": "Novo terminal",
"command.terminal.new.description": "Criar uma nova aba de terminal",
@@ -100,7 +104,7 @@ export const dict = {
"dialog.model.unpaid.freeModels.title": "Modelos gratuitos fornecidos pelo OpenCode",
"dialog.model.unpaid.addMore.title": "Adicionar mais modelos de provedores populares",
"dialog.provider.viewAll": "Ver todos os provedores",
"dialog.provider.viewAll": "Ver mais provedores",
"provider.connect.title": "Conectar {{provider}}",
"provider.connect.title.anthropicProMax": "Entrar com Claude Pro/Max",
@@ -137,6 +141,8 @@ export const dict = {
"provider.connect.toast.connected.title": "{{provider}} conectado",
"provider.connect.toast.connected.description": "Modelos do {{provider}} agora estão disponíveis para uso.",
"provider.disconnect.toast.disconnected.title": "{{provider}} desconectado",
"provider.disconnect.toast.disconnected.description": "Os modelos de {{provider}} não estão mais disponíveis.",
"model.tag.free": "Grátis",
"model.tag.latest": "Mais recente",
"model.provider.anthropic": "Anthropic",
@@ -159,6 +165,8 @@ export const dict = {
"common.loading": "Carregando",
"common.loading.ellipsis": "...",
"common.cancel": "Cancelar",
"common.connect": "Conectar",
"common.disconnect": "Desconectar",
"common.submit": "Enviar",
"common.save": "Salvar",
"common.saving": "Salvando...",
@@ -167,6 +175,8 @@ export const dict = {
"prompt.placeholder.shell": "Digite comando do shell...",
"prompt.placeholder.normal": 'Pergunte qualquer coisa... "{{example}}"',
"prompt.placeholder.summarizeComments": "Resumir comentários…",
"prompt.placeholder.summarizeComment": "Resumir comentário…",
"prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "esc para sair",
@@ -223,6 +233,8 @@ export const dict = {
"dialog.mcp.description": "{{enabled}} de {{total}} habilitados",
"dialog.mcp.empty": "Nenhum MCP configurado",
"dialog.lsp.empty": "LSPs detectados automaticamente pelos tipos de arquivo",
"dialog.plugins.empty": "Plugins configurados em opencode.json",
"mcp.status.connected": "conectado",
"mcp.status.failed": "falhou",
"mcp.status.needs_auth": "precisa de autenticação",
@@ -251,6 +263,12 @@ export const dict = {
"dialog.server.default.clear": "Limpar",
"dialog.server.action.remove": "Remover servidor",
"dialog.server.menu.edit": "Editar",
"dialog.server.menu.default": "Definir como padrão",
"dialog.server.menu.defaultRemove": "Remover padrão",
"dialog.server.menu.delete": "Excluir",
"dialog.server.current": "Servidor atual",
"dialog.server.status.default": "Padrão",
"dialog.project.edit.title": "Editar projeto",
"dialog.project.edit.name": "Nome",
"dialog.project.edit.icon": "Ícone",
@@ -298,20 +316,21 @@ export const dict = {
"context.usage.clickToView": "Clique para ver o contexto",
"context.usage.view": "Ver uso do contexto",
"language.en": "Inglês",
"language.zh": "Chinês (Simplificado)",
"language.zht": "Chinês (Tradicional)",
"language.ko": "Coreano",
"language.de": "Alemão",
"language.es": "Espanhol",
"language.fr": "Frans",
"language.ja": "Japonês",
"language.da": "Dinamarquês",
"language.ru": "Russo",
"language.pl": "Polonês",
"language.ar": "Árabe",
"language.no": "Norueguês",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Idioma",
"toast.language.description": "Alterado para {{language}}",
@@ -329,6 +348,9 @@ export const dict = {
"toast.file.loadFailed.title": "Falha ao carregar arquivo",
"toast.file.listFailed.title": "Falha ao listar arquivos",
"toast.context.noLineSelection.title": "Nenhuma seleção de linhas",
"toast.context.noLineSelection.description": "Selecione primeiro um intervalo de linhas em uma aba de arquivo.",
"toast.session.share.copyFailed.title": "Falha ao copiar URL para a área de transferência",
"toast.session.share.success.title": "Sessão compartilhada",
"toast.session.share.success.description": "URL compartilhada copiada para a área de transferência!",
@@ -404,8 +426,13 @@ export const dict = {
"session.tab.context": "Contexto",
"session.panel.reviewAndFiles": "Revisão e arquivos",
"session.review.filesChanged": "{{count}} Arquivos Alterados",
"session.review.change.one": "Alteração",
"session.review.change.other": "Alterações",
"session.review.loadingChanges": "Carregando alterações...",
"session.review.empty": "Nenhuma alteração nesta sessão ainda",
"session.review.noChanges": "Sem alterações",
"session.files.selectToOpen": "Selecione um arquivo para abrir",
"session.files.all": "Todos os arquivos",
"session.messages.renderEarlier": "Renderizar mensagens anteriores",
"session.messages.loadingEarlier": "Carregando mensagens anteriores...",
"session.messages.loadEarlier": "Carregar mensagens anteriores",
@@ -422,6 +449,14 @@ export const dict = {
"session.header.search.placeholder": "Buscar {{project}}",
"session.header.searchFiles": "Buscar arquivos",
"status.popover.trigger": "Status",
"status.popover.ariaLabel": "Configurações de servidores",
"status.popover.tab.servers": "Servidores",
"status.popover.tab.mcp": "MCP",
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Plugins",
"status.popover.action.manageServers": "Gerenciar servidores",
"session.share.popover.title": "Publicar na web",
"session.share.popover.description.shared":
"Esta sessão é pública na web. Está acessível para qualquer pessoa com o link.",
@@ -474,12 +509,15 @@ export const dict = {
"sidebar.project.recentSessions": "Sessões recentes",
"sidebar.project.viewAllSessions": "Ver todas as sessões",
"app.name.desktop": "OpenCode Desktop",
"settings.section.desktop": "Desktop",
"settings.section.server": "Servidor",
"settings.tab.general": "Geral",
"settings.tab.shortcuts": "Atalhos",
"settings.general.section.appearance": "Aparência",
"settings.general.section.notifications": "Notificações do sistema",
"settings.general.section.updates": "Atualizações",
"settings.general.section.sounds": "Efeitos sonoros",
"settings.general.row.language.title": "Idioma",
@@ -490,12 +528,25 @@ export const dict = {
"settings.general.row.theme.description": "Personalize como o OpenCode é tematizado.",
"settings.general.row.font.title": "Fonte",
"settings.general.row.font.description": "Personalize a fonte monoespaçada usada em blocos de código",
"settings.general.row.releaseNotes.title": "Notas da versão",
"settings.general.row.releaseNotes.description": 'Mostrar pop-ups de "Novidades" após atualizações',
"settings.updates.row.startup.title": "Verificar atualizações ao iniciar",
"settings.updates.row.startup.description": "Verificar atualizações automaticamente quando o OpenCode iniciar",
"settings.updates.row.check.title": "Verificar atualizações",
"settings.updates.row.check.description": "Verificar atualizações manualmente e instalar se houver",
"settings.updates.action.checkNow": "Verificar agora",
"settings.updates.action.checking": "Verificando...",
"settings.updates.toast.latest.title": "Você está atualizado",
"settings.updates.toast.latest.description": "Você está usando a versão mais recente do OpenCode.",
"font.option.ibmPlexMono": "IBM Plex Mono",
"font.option.cascadiaCode": "Cascadia Code",
"font.option.firaCode": "Fira Code",
"font.option.hack": "Hack",
"font.option.inconsolata": "Inconsolata",
"font.option.intelOneMono": "Intel One Mono",
"font.option.iosevka": "Iosevka",
"font.option.jetbrainsMono": "JetBrains Mono",
"font.option.mesloLgs": "Meslo LGS",
"font.option.robotoMono": "Roboto Mono",
@@ -583,6 +634,13 @@ export const dict = {
"settings.providers.title": "Provedores",
"settings.providers.description": "Configurações de provedores estarão disponíveis aqui.",
"settings.providers.section.connected": "Provedores conectados",
"settings.providers.connected.empty": "Nenhum provedor conectado",
"settings.providers.section.popular": "Provedores populares",
"settings.providers.tag.environment": "Ambiente",
"settings.providers.tag.config": "Configuração",
"settings.providers.tag.custom": "Personalizado",
"settings.providers.tag.other": "Outro",
"settings.models.title": "Modelos",
"settings.models.description": "Configurações de modelos estarão disponíveis aqui.",
"settings.agents.title": "Agentes",
@@ -650,6 +708,7 @@ export const dict = {
"workspace.reset.failed.title": "Falha ao redefinir espaço de trabalho",
"workspace.reset.success.title": "Espaço de trabalho redefinido",
"workspace.reset.success.description": "Espaço de trabalho agora corresponde ao branch padrão.",
"workspace.error.stillPreparing": "O espaço de trabalho ainda está sendo preparado",
"workspace.status.checking": "Verificando alterações não mescladas...",
"workspace.status.error": "Não foi possível verificar o status do git.",
"workspace.status.clean": "Nenhuma alteração não mesclada detectada.",
+146 -13
View File
@@ -8,6 +8,7 @@ export const dict = {
"command.category.theme": "Tema",
"command.category.language": "Sprog",
"command.category.file": "Fil",
"command.category.context": "Kontekst",
"command.category.terminal": "Terminal",
"command.category.model": "Model",
"command.category.mcp": "MCP",
@@ -15,6 +16,7 @@ export const dict = {
"command.category.permissions": "Tilladelser",
"command.category.workspace": "Arbejdsområde",
"command.category.settings": "Indstillinger",
"theme.scheme.system": "System",
"theme.scheme.light": "Lys",
"theme.scheme.dark": "Mørk",
@@ -23,6 +25,7 @@ export const dict = {
"command.project.open": "Åbn projekt",
"command.provider.connect": "Tilslut udbyder",
"command.server.switch": "Skift server",
"command.settings.open": "Åbn indstillinger",
"command.session.previous": "Forrige session",
"command.session.next": "Næste session",
"command.session.archive": "Arkivér session",
@@ -40,7 +43,10 @@ export const dict = {
"command.session.new": "Ny session",
"command.file.open": "Åbn fil",
"command.file.open.description": "Søg i filer og kommandoer",
"command.context.addSelection": "Tilføj markering til kontekst",
"command.context.addSelection.description": "Tilføj markerede linjer fra den aktuelle fil",
"command.terminal.toggle": "Skift terminal",
"command.fileTree.toggle": "Skift filtræ",
"command.review.toggle": "Skift gennemgang",
"command.terminal.new": "Ny terminal",
"command.terminal.new.description": "Opret en ny terminalfane",
@@ -98,7 +104,7 @@ export const dict = {
"dialog.model.unpaid.freeModels.title": "Gratis modeller leveret af OpenCode",
"dialog.model.unpaid.addMore.title": "Tilføj flere modeller fra populære udbydere",
"dialog.provider.viewAll": "Vis alle udbydere",
"dialog.provider.viewAll": "Vis flere udbydere",
"provider.connect.title": "Forbind {{provider}}",
"provider.connect.title.anthropicProMax": "Log ind med Claude Pro/Max",
@@ -117,6 +123,7 @@ export const dict = {
"provider.connect.opencodeZen.line2":
"Med en enkelt API-nøgle får du adgang til modeller som Claude, GPT, Gemini, GLM og flere.",
"provider.connect.opencodeZen.visit.prefix": "Besøg ",
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
"provider.connect.opencodeZen.visit.suffix": " for at hente din API-nøgle.",
"provider.connect.oauth.code.visit.prefix": "Besøg ",
"provider.connect.oauth.code.visit.link": "dette link",
@@ -134,13 +141,32 @@ export const dict = {
"provider.connect.toast.connected.title": "{{provider}} forbundet",
"provider.connect.toast.connected.description": "{{provider}} modeller er nu tilgængelige.",
"provider.disconnect.toast.disconnected.title": "{{provider}} frakoblet",
"provider.disconnect.toast.disconnected.description": "Modeller fra {{provider}} er ikke længere tilgængelige.",
"model.tag.free": "Gratis",
"model.tag.latest": "Nyeste",
"model.provider.anthropic": "Anthropic",
"model.provider.openai": "OpenAI",
"model.provider.google": "Google",
"model.provider.xai": "xAI",
"model.provider.meta": "Meta",
"model.input.text": "tekst",
"model.input.image": "billede",
"model.input.audio": "lyd",
"model.input.video": "video",
"model.input.pdf": "pdf",
"model.tooltip.allows": "Tillader: {{inputs}}",
"model.tooltip.reasoning.allowed": "Tillader tænkning",
"model.tooltip.reasoning.none": "Ingen tænkning",
"model.tooltip.context": "Kontekstgrænse {{limit}}",
"common.search.placeholder": "Søg",
"common.goBack": "Gå tilbage",
"common.loading": "Indlæser",
"common.loading.ellipsis": "...",
"common.cancel": "Annuller",
"common.connect": "Forbind",
"common.disconnect": "Frakobl",
"common.submit": "Indsend",
"common.save": "Gem",
"common.saving": "Gemmer...",
@@ -149,6 +175,8 @@ export const dict = {
"prompt.placeholder.shell": "Indtast shell-kommando...",
"prompt.placeholder.normal": 'Spørg om hvad som helst... "{{example}}"',
"prompt.placeholder.summarizeComments": "Opsummér kommentarer…",
"prompt.placeholder.summarizeComment": "Opsummér kommentar…",
"prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "esc for at afslutte",
@@ -252,6 +280,9 @@ export const dict = {
"dialog.project.edit.color": "Farve",
"dialog.project.edit.color.select": "Vælg farven {{color}}",
"dialog.project.edit.worktree.startup": "Opstartsscript for arbejdsområde",
"dialog.project.edit.worktree.startup.description": "Køres efter oprettelse af et nyt arbejdsområde (worktree).",
"dialog.project.edit.worktree.startup.placeholder": "f.eks. bun install",
"context.breakdown.title": "Kontekstfordeling",
"context.breakdown.note":
'Omtrentlig fordeling af input-tokens. "Andre" inkluderer værktøjsdefinitioner og overhead.',
@@ -287,20 +318,21 @@ export const dict = {
"context.usage.clickToView": "Klik for at se kontekst",
"context.usage.view": "Se kontekstforbrug",
"language.en": "Engelsk",
"language.zh": "Kinesisk (forenklet)",
"language.zht": "Kinesisk (traditionelt)",
"language.ko": "Koreansk",
"language.de": "Tysk",
"language.es": "Spansk",
"language.fr": "Fransk",
"language.ja": "Japansk",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ru": "Russisk",
"language.pl": "Polsk",
"language.ar": "Arabisk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Portugisisk (Brasilien)",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Sprog",
"toast.language.description": "Skiftede til {{language}}",
@@ -318,6 +350,9 @@ export const dict = {
"toast.file.loadFailed.title": "Kunne ikke indlæse fil",
"toast.file.listFailed.title": "Kunne ikke liste filer",
"toast.context.noLineSelection.title": "Ingen linjevalg",
"toast.context.noLineSelection.description": "Vælg først et linjeinterval i en filfane.",
"toast.session.share.copyFailed.title": "Kunne ikke kopiere URL til udklipsholder",
"toast.session.share.success.title": "Session delt",
"toast.session.share.success.description": "Delings-URL kopieret til udklipsholder!",
@@ -392,13 +427,19 @@ export const dict = {
"session.tab.context": "Kontekst",
"session.panel.reviewAndFiles": "Gennemgang og filer",
"session.review.filesChanged": "{{count}} Filer ændret",
"session.review.change.one": "Ændring",
"session.review.change.other": "Ændringer",
"session.review.loadingChanges": "Indlæser ændringer...",
"session.review.empty": "Ingen ændringer i denne session endnu",
"session.review.noChanges": "Ingen ændringer",
"session.files.selectToOpen": "Vælg en fil at åbne",
"session.files.all": "Alle filer",
"session.messages.renderEarlier": "Vis tidligere beskeder",
"session.messages.loadingEarlier": "Indlæser tidligere beskeder...",
"session.messages.loadEarlier": "Indlæs tidligere beskeder",
"session.messages.loading": "Indlæser beskeder...",
"session.messages.jumpToLatest": "Gå til seneste",
"session.context.addToContext": "Tilføj {{selection}} til kontekst",
"session.new.worktree.main": "Hovedgren",
@@ -409,6 +450,14 @@ export const dict = {
"session.header.search.placeholder": "Søg {{project}}",
"session.header.searchFiles": "Søg efter filer",
"status.popover.trigger": "Status",
"status.popover.ariaLabel": "Serverkonfigurationer",
"status.popover.tab.servers": "Servere",
"status.popover.tab.mcp": "MCP",
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Plugins",
"status.popover.action.manageServers": "Administrer servere",
"session.share.popover.title": "Udgiv på nettet",
"session.share.popover.description.shared":
"Denne session er offentlig på nettet. Den er tilgængelig for alle med linket.",
@@ -432,6 +481,8 @@ export const dict = {
"terminal.title.numbered": "Terminal {{number}}",
"terminal.close": "Luk terminal",
"terminal.connectionLost.title": "Forbindelse mistet",
"terminal.connectionLost.description": "Terminalforbindelsen blev afbrudt. Dette kan ske, når serveren genstarter.",
"common.closeTab": "Luk fane",
"common.dismiss": "Afvis",
"common.requestFailed": "Forespørgsel mislykkedes",
@@ -445,6 +496,8 @@ export const dict = {
"common.edit": "Rediger",
"common.loadMore": "Indlæs flere",
"common.key.esc": "ESC",
"sidebar.menu.toggle": "Skift menu",
"sidebar.nav.projectsAndSessions": "Projekter og sessioner",
"sidebar.settings": "Indstillinger",
"sidebar.help": "Hjælp",
@@ -456,12 +509,15 @@ export const dict = {
"sidebar.project.recentSessions": "Seneste sessioner",
"sidebar.project.viewAllSessions": "Vis alle sessioner",
"app.name.desktop": "OpenCode Desktop",
"settings.section.desktop": "Desktop",
"settings.section.server": "Server",
"settings.tab.general": "Generelt",
"settings.tab.shortcuts": "Genveje",
"settings.general.section.appearance": "Udseende",
"settings.general.section.notifications": "Systemmeddelelser",
"settings.general.section.updates": "Opdateringer",
"settings.general.section.sounds": "Lydeffekter",
"settings.general.row.language.title": "Sprog",
@@ -473,6 +529,75 @@ export const dict = {
"settings.general.row.font.title": "Skrifttype",
"settings.general.row.font.description": "Tilpas mono-skrifttypen brugt i kodeblokke",
"settings.general.row.releaseNotes.title": "Udgivelsesnoter",
"settings.general.row.releaseNotes.description": 'Vis "Hvad er nyt"-popups efter opdateringer',
"settings.updates.row.startup.title": "Tjek for opdateringer ved opstart",
"settings.updates.row.startup.description": "Tjek automatisk for opdateringer, når OpenCode starter",
"settings.updates.row.check.title": "Tjek for opdateringer",
"settings.updates.row.check.description": "Tjek manuelt for opdateringer og installer, hvis tilgængelig",
"settings.updates.action.checkNow": "Tjek nu",
"settings.updates.action.checking": "Tjekker...",
"settings.updates.toast.latest.title": "Du er opdateret",
"settings.updates.toast.latest.description": "Du kører den nyeste version af OpenCode.",
"font.option.ibmPlexMono": "IBM Plex Mono",
"font.option.cascadiaCode": "Cascadia Code",
"font.option.firaCode": "Fira Code",
"font.option.hack": "Hack",
"font.option.inconsolata": "Inconsolata",
"font.option.intelOneMono": "Intel One Mono",
"font.option.iosevka": "Iosevka",
"font.option.jetbrainsMono": "JetBrains Mono",
"font.option.mesloLgs": "Meslo LGS",
"font.option.robotoMono": "Roboto Mono",
"font.option.sourceCodePro": "Source Code Pro",
"font.option.ubuntuMono": "Ubuntu Mono",
"sound.option.alert01": "Alarm 01",
"sound.option.alert02": "Alarm 02",
"sound.option.alert03": "Alarm 03",
"sound.option.alert04": "Alarm 04",
"sound.option.alert05": "Alarm 05",
"sound.option.alert06": "Alarm 06",
"sound.option.alert07": "Alarm 07",
"sound.option.alert08": "Alarm 08",
"sound.option.alert09": "Alarm 09",
"sound.option.alert10": "Alarm 10",
"sound.option.bipbop01": "Bip-bop 01",
"sound.option.bipbop02": "Bip-bop 02",
"sound.option.bipbop03": "Bip-bop 03",
"sound.option.bipbop04": "Bip-bop 04",
"sound.option.bipbop05": "Bip-bop 05",
"sound.option.bipbop06": "Bip-bop 06",
"sound.option.bipbop07": "Bip-bop 07",
"sound.option.bipbop08": "Bip-bop 08",
"sound.option.bipbop09": "Bip-bop 09",
"sound.option.bipbop10": "Bip-bop 10",
"sound.option.staplebops01": "Staplebops 01",
"sound.option.staplebops02": "Staplebops 02",
"sound.option.staplebops03": "Staplebops 03",
"sound.option.staplebops04": "Staplebops 04",
"sound.option.staplebops05": "Staplebops 05",
"sound.option.staplebops06": "Staplebops 06",
"sound.option.staplebops07": "Staplebops 07",
"sound.option.nope01": "Nej 01",
"sound.option.nope02": "Nej 02",
"sound.option.nope03": "Nej 03",
"sound.option.nope04": "Nej 04",
"sound.option.nope05": "Nej 05",
"sound.option.nope06": "Nej 06",
"sound.option.nope07": "Nej 07",
"sound.option.nope08": "Nej 08",
"sound.option.nope09": "Nej 09",
"sound.option.nope10": "Nej 10",
"sound.option.nope11": "Nej 11",
"sound.option.nope12": "Nej 12",
"sound.option.yup01": "Ja 01",
"sound.option.yup02": "Ja 02",
"sound.option.yup03": "Ja 03",
"sound.option.yup04": "Ja 04",
"sound.option.yup05": "Ja 05",
"sound.option.yup06": "Ja 06",
"settings.general.notifications.agent.title": "Agent",
"settings.general.notifications.agent.description":
"Vis systemmeddelelse når agenten er færdig eller kræver opmærksomhed",
@@ -508,6 +633,13 @@ export const dict = {
"settings.providers.title": "Udbydere",
"settings.providers.description": "Udbyderindstillinger vil kunne konfigureres her.",
"settings.providers.section.connected": "Forbundne udbydere",
"settings.providers.connected.empty": "Ingen forbundne udbydere",
"settings.providers.section.popular": "Populære udbydere",
"settings.providers.tag.environment": "Miljø",
"settings.providers.tag.config": "Konfiguration",
"settings.providers.tag.custom": "Brugerdefineret",
"settings.providers.tag.other": "Andet",
"settings.models.title": "Modeller",
"settings.models.description": "Modelindstillinger vil kunne konfigureres her.",
"settings.agents.title": "Agenter",
@@ -575,6 +707,7 @@ export const dict = {
"workspace.reset.failed.title": "Kunne ikke nulstille arbejdsområde",
"workspace.reset.success.title": "Arbejdsområde nulstillet",
"workspace.reset.success.description": "Arbejdsområdet matcher nu hovedgrenen.",
"workspace.error.stillPreparing": "Arbejdsområdet er stadig ved at blive klargjort",
"workspace.status.checking": "Tjekker for uflettede ændringer...",
"workspace.status.error": "Kunne ikke bekræfte git-status.",
"workspace.status.clean": "Ingen uflettede ændringer fundet.",
+149 -14
View File
@@ -12,6 +12,7 @@ export const dict = {
"command.category.theme": "Thema",
"command.category.language": "Sprache",
"command.category.file": "Datei",
"command.category.context": "Kontext",
"command.category.terminal": "Terminal",
"command.category.model": "Modell",
"command.category.mcp": "MCP",
@@ -19,6 +20,7 @@ export const dict = {
"command.category.permissions": "Berechtigungen",
"command.category.workspace": "Arbeitsbereich",
"command.category.settings": "Einstellungen",
"theme.scheme.system": "System",
"theme.scheme.light": "Hell",
"theme.scheme.dark": "Dunkel",
@@ -27,6 +29,7 @@ export const dict = {
"command.project.open": "Projekt öffnen",
"command.provider.connect": "Anbieter verbinden",
"command.server.switch": "Server wechseln",
"command.settings.open": "Einstellungen öffnen",
"command.session.previous": "Vorherige Sitzung",
"command.session.next": "Nächste Sitzung",
"command.session.archive": "Sitzung archivieren",
@@ -44,7 +47,10 @@ export const dict = {
"command.session.new": "Neue Sitzung",
"command.file.open": "Datei öffnen",
"command.file.open.description": "Dateien und Befehle durchsuchen",
"command.context.addSelection": "Auswahl zum Kontext hinzufügen",
"command.context.addSelection.description": "Ausgewählte Zeilen aus der aktuellen Datei hinzufügen",
"command.terminal.toggle": "Terminal umschalten",
"command.fileTree.toggle": "Dateibaum umschalten",
"command.review.toggle": "Überprüfung umschalten",
"command.terminal.new": "Neues Terminal",
"command.terminal.new.description": "Neuen Terminal-Tab erstellen",
@@ -102,7 +108,7 @@ export const dict = {
"dialog.model.unpaid.freeModels.title": "Kostenlose Modelle von OpenCode",
"dialog.model.unpaid.addMore.title": "Weitere Modelle von beliebten Anbietern hinzufügen",
"dialog.provider.viewAll": "Alle Anbieter anzeigen",
"dialog.provider.viewAll": "Mehr Anbieter anzeigen",
"provider.connect.title": "{{provider}} verbinden",
"provider.connect.title.anthropicProMax": "Mit Claude Pro/Max anmelden",
@@ -121,6 +127,7 @@ export const dict = {
"provider.connect.opencodeZen.line2":
"Mit einem einzigen API-Schlüssel erhalten Sie Zugriff auf Modelle wie Claude, GPT, Gemini, GLM und mehr.",
"provider.connect.opencodeZen.visit.prefix": "Besuchen Sie ",
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
"provider.connect.opencodeZen.visit.suffix": ", um Ihren API-Schlüssel zu erhalten.",
"provider.connect.oauth.code.visit.prefix": "Besuchen Sie ",
"provider.connect.oauth.code.visit.link": "diesen Link",
@@ -138,13 +145,32 @@ export const dict = {
"provider.connect.toast.connected.title": "{{provider}} verbunden",
"provider.connect.toast.connected.description": "{{provider}} Modelle sind jetzt verfügbar.",
"provider.disconnect.toast.disconnected.title": "{{provider}} getrennt",
"provider.disconnect.toast.disconnected.description": "Die {{provider}}-Modelle sind nicht mehr verfügbar.",
"model.tag.free": "Kostenlos",
"model.tag.latest": "Neueste",
"model.provider.anthropic": "Anthropic",
"model.provider.openai": "OpenAI",
"model.provider.google": "Google",
"model.provider.xai": "xAI",
"model.provider.meta": "Meta",
"model.input.text": "Text",
"model.input.image": "Bild",
"model.input.audio": "Audio",
"model.input.video": "Video",
"model.input.pdf": "pdf",
"model.tooltip.allows": "Erlaubt: {{inputs}}",
"model.tooltip.reasoning.allowed": "Erlaubt Reasoning",
"model.tooltip.reasoning.none": "Kein Reasoning",
"model.tooltip.context": "Kontextlimit {{limit}}",
"common.search.placeholder": "Suchen",
"common.goBack": "Zurück",
"common.loading": "Laden",
"common.loading.ellipsis": "...",
"common.cancel": "Abbrechen",
"common.connect": "Verbinden",
"common.disconnect": "Trennen",
"common.submit": "Absenden",
"common.save": "Speichern",
"common.saving": "Speichert...",
@@ -153,6 +179,8 @@ export const dict = {
"prompt.placeholder.shell": "Shell-Befehl eingeben...",
"prompt.placeholder.normal": 'Fragen Sie alles... "{{example}}"',
"prompt.placeholder.summarizeComments": "Kommentare zusammenfassen…",
"prompt.placeholder.summarizeComment": "Kommentar zusammenfassen…",
"prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "esc zum Verlassen",
@@ -257,6 +285,10 @@ export const dict = {
"dialog.project.edit.color": "Farbe",
"dialog.project.edit.color.select": "{{color}}-Farbe auswählen",
"dialog.project.edit.worktree.startup": "Startup-Skript für Arbeitsbereich",
"dialog.project.edit.worktree.startup.description":
"Wird nach dem Erstellen eines neuen Arbeitsbereichs (Worktree) ausgeführt.",
"dialog.project.edit.worktree.startup.placeholder": "z. B. bun install",
"context.breakdown.title": "Kontext-Aufschlüsselung",
"context.breakdown.note":
'Ungefähre Aufschlüsselung der Eingabe-Token. "Andere" beinhaltet Werkzeugdefinitionen und Overhead.',
@@ -292,20 +324,21 @@ export const dict = {
"context.usage.clickToView": "Klicken, um Kontext anzuzeigen",
"context.usage.view": "Kontextnutzung anzeigen",
"language.en": "Englisch",
"language.zh": "Chinesisch (Vereinfacht)",
"language.zht": "Chinesisch (Traditionell)",
"language.ko": "Koreanisch",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Spanisch",
"language.fr": "Französisch",
"language.ja": "Japanisch",
"language.da": "Dänisch",
"language.ru": "Russisch",
"language.pl": "Polnisch",
"language.ar": "Arabisch",
"language.no": "Norwegisch",
"language.br": "Portugiesisch (Brasilien)",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Sprache",
"toast.language.description": "Zu {{language}} gewechselt",
@@ -323,6 +356,9 @@ export const dict = {
"toast.file.loadFailed.title": "Datei konnte nicht geladen werden",
"toast.file.listFailed.title": "Dateien konnten nicht aufgelistet werden",
"toast.context.noLineSelection.title": "Keine Zeilenauswahl",
"toast.context.noLineSelection.description": "Wählen Sie zuerst einen Zeilenbereich in einem Datei-Tab aus.",
"toast.session.share.copyFailed.title": "URL konnte nicht in die Zwischenablage kopiert werden",
"toast.session.share.success.title": "Sitzung geteilt",
"toast.session.share.success.description": "Teilen-URL in die Zwischenablage kopiert!",
@@ -399,13 +435,19 @@ export const dict = {
"session.tab.context": "Kontext",
"session.panel.reviewAndFiles": "Überprüfung und Dateien",
"session.review.filesChanged": "{{count}} Dateien geändert",
"session.review.change.one": "Änderung",
"session.review.change.other": "Änderungen",
"session.review.loadingChanges": "Lade Änderungen...",
"session.review.empty": "Noch keine Änderungen in dieser Sitzung",
"session.review.noChanges": "Keine Änderungen",
"session.files.selectToOpen": "Datei zum Öffnen auswählen",
"session.files.all": "Alle Dateien",
"session.messages.renderEarlier": "Frühere Nachrichten rendern",
"session.messages.loadingEarlier": "Lade frühere Nachrichten...",
"session.messages.loadEarlier": "Frühere Nachrichten laden",
"session.messages.loading": "Lade Nachrichten...",
"session.messages.jumpToLatest": "Zum neuesten springen",
"session.context.addToContext": "{{selection}} zum Kontext hinzufügen",
"session.new.worktree.main": "Haupt-Branch",
@@ -416,6 +458,14 @@ export const dict = {
"session.header.search.placeholder": "{{project}} durchsuchen",
"session.header.searchFiles": "Dateien suchen",
"status.popover.trigger": "Status",
"status.popover.ariaLabel": "Serverkonfigurationen",
"status.popover.tab.servers": "Server",
"status.popover.tab.mcp": "MCP",
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Plugins",
"status.popover.action.manageServers": "Server verwalten",
"session.share.popover.title": "Im Web veröffentlichen",
"session.share.popover.description.shared":
"Diese Sitzung ist öffentlich im Web. Sie ist für jeden mit dem Link zugänglich.",
@@ -439,6 +489,9 @@ export const dict = {
"terminal.title.numbered": "Terminal {{number}}",
"terminal.close": "Terminal schließen",
"terminal.connectionLost.title": "Verbindung verloren",
"terminal.connectionLost.description":
"Die Terminalverbindung wurde unterbrochen. Das kann passieren, wenn der Server neu startet.",
"common.closeTab": "Tab schließen",
"common.dismiss": "Verwerfen",
"common.requestFailed": "Anfrage fehlgeschlagen",
@@ -452,6 +505,8 @@ export const dict = {
"common.edit": "Bearbeiten",
"common.loadMore": "Mehr laden",
"common.key.esc": "ESC",
"sidebar.menu.toggle": "Menü umschalten",
"sidebar.nav.projectsAndSessions": "Projekte und Sitzungen",
"sidebar.settings": "Einstellungen",
"sidebar.help": "Hilfe",
@@ -464,12 +519,15 @@ export const dict = {
"sidebar.project.recentSessions": "Letzte Sitzungen",
"sidebar.project.viewAllSessions": "Alle Sitzungen anzeigen",
"app.name.desktop": "OpenCode Desktop",
"settings.section.desktop": "Desktop",
"settings.section.server": "Server",
"settings.tab.general": "Allgemein",
"settings.tab.shortcuts": "Tastenkombinationen",
"settings.general.section.appearance": "Erscheinungsbild",
"settings.general.section.notifications": "Systembenachrichtigungen",
"settings.general.section.updates": "Updates",
"settings.general.section.sounds": "Soundeffekte",
"settings.general.row.language.title": "Sprache",
@@ -481,6 +539,75 @@ export const dict = {
"settings.general.row.font.title": "Schriftart",
"settings.general.row.font.description": "Die in Codeblöcken verwendete Monospace-Schriftart anpassen",
"settings.general.row.releaseNotes.title": "Versionshinweise",
"settings.general.row.releaseNotes.description": '"Neuigkeiten"-Pop-ups nach Updates anzeigen',
"settings.updates.row.startup.title": "Beim Start nach Updates suchen",
"settings.updates.row.startup.description": "Beim Start von OpenCode automatisch nach Updates suchen",
"settings.updates.row.check.title": "Nach Updates suchen",
"settings.updates.row.check.description": "Manuell nach Updates suchen und installieren, wenn verfügbar",
"settings.updates.action.checkNow": "Jetzt prüfen",
"settings.updates.action.checking": "Wird geprüft...",
"settings.updates.toast.latest.title": "Du bist auf dem neuesten Stand",
"settings.updates.toast.latest.description": "Du verwendest die aktuelle Version von OpenCode.",
"font.option.ibmPlexMono": "IBM Plex Mono",
"font.option.cascadiaCode": "Cascadia Code",
"font.option.firaCode": "Fira Code",
"font.option.hack": "Hack",
"font.option.inconsolata": "Inconsolata",
"font.option.intelOneMono": "Intel One Mono",
"font.option.iosevka": "Iosevka",
"font.option.jetbrainsMono": "JetBrains Mono",
"font.option.mesloLgs": "Meslo LGS",
"font.option.robotoMono": "Roboto Mono",
"font.option.sourceCodePro": "Source Code Pro",
"font.option.ubuntuMono": "Ubuntu Mono",
"sound.option.alert01": "Alarm 01",
"sound.option.alert02": "Alarm 02",
"sound.option.alert03": "Alarm 03",
"sound.option.alert04": "Alarm 04",
"sound.option.alert05": "Alarm 05",
"sound.option.alert06": "Alarm 06",
"sound.option.alert07": "Alarm 07",
"sound.option.alert08": "Alarm 08",
"sound.option.alert09": "Alarm 09",
"sound.option.alert10": "Alarm 10",
"sound.option.bipbop01": "Bip-bop 01",
"sound.option.bipbop02": "Bip-bop 02",
"sound.option.bipbop03": "Bip-bop 03",
"sound.option.bipbop04": "Bip-bop 04",
"sound.option.bipbop05": "Bip-bop 05",
"sound.option.bipbop06": "Bip-bop 06",
"sound.option.bipbop07": "Bip-bop 07",
"sound.option.bipbop08": "Bip-bop 08",
"sound.option.bipbop09": "Bip-bop 09",
"sound.option.bipbop10": "Bip-bop 10",
"sound.option.staplebops01": "Staplebops 01",
"sound.option.staplebops02": "Staplebops 02",
"sound.option.staplebops03": "Staplebops 03",
"sound.option.staplebops04": "Staplebops 04",
"sound.option.staplebops05": "Staplebops 05",
"sound.option.staplebops06": "Staplebops 06",
"sound.option.staplebops07": "Staplebops 07",
"sound.option.nope01": "Nein 01",
"sound.option.nope02": "Nein 02",
"sound.option.nope03": "Nein 03",
"sound.option.nope04": "Nein 04",
"sound.option.nope05": "Nein 05",
"sound.option.nope06": "Nein 06",
"sound.option.nope07": "Nein 07",
"sound.option.nope08": "Nein 08",
"sound.option.nope09": "Nein 09",
"sound.option.nope10": "Nein 10",
"sound.option.nope11": "Nein 11",
"sound.option.nope12": "Nein 12",
"sound.option.yup01": "Ja 01",
"sound.option.yup02": "Ja 02",
"sound.option.yup03": "Ja 03",
"sound.option.yup04": "Ja 04",
"sound.option.yup05": "Ja 05",
"sound.option.yup06": "Ja 06",
"settings.general.notifications.agent.title": "Agent",
"settings.general.notifications.agent.description":
"Systembenachrichtigung anzeigen, wenn der Agent fertig ist oder Aufmerksamkeit benötigt",
@@ -517,6 +644,13 @@ export const dict = {
"settings.providers.title": "Anbieter",
"settings.providers.description": "Anbietereinstellungen können hier konfiguriert werden.",
"settings.providers.section.connected": "Verbundene Anbieter",
"settings.providers.connected.empty": "Keine verbundenen Anbieter",
"settings.providers.section.popular": "Beliebte Anbieter",
"settings.providers.tag.environment": "Umgebung",
"settings.providers.tag.config": "Konfiguration",
"settings.providers.tag.custom": "Benutzerdefiniert",
"settings.providers.tag.other": "Andere",
"settings.models.title": "Modelle",
"settings.models.description": "Modelleinstellungen können hier konfiguriert werden.",
"settings.agents.title": "Agenten",
@@ -584,6 +718,7 @@ export const dict = {
"workspace.reset.failed.title": "Arbeitsbereich konnte nicht zurückgesetzt werden",
"workspace.reset.success.title": "Arbeitsbereich zurückgesetzt",
"workspace.reset.success.description": "Der Arbeitsbereich entspricht jetzt dem Standard-Branch.",
"workspace.error.stillPreparing": "Arbeitsbereich wird noch vorbereitet",
"workspace.status.checking": "Suche nach nicht zusammengeführten Änderungen...",
"workspace.status.error": "Git-Status konnte nicht überprüft werden.",
"workspace.status.clean": "Keine nicht zusammengeführten Änderungen erkannt.",
+78 -18
View File
@@ -8,6 +8,7 @@ export const dict = {
"command.category.theme": "Theme",
"command.category.language": "Language",
"command.category.file": "File",
"command.category.context": "Context",
"command.category.terminal": "Terminal",
"command.category.model": "Model",
"command.category.mcp": "MCP",
@@ -42,7 +43,10 @@ export const dict = {
"command.session.new": "New session",
"command.file.open": "Open file",
"command.file.open.description": "Search files and commands",
"command.context.addSelection": "Add selection to context",
"command.context.addSelection.description": "Add selected lines from the current file",
"command.terminal.toggle": "Toggle terminal",
"command.fileTree.toggle": "Toggle file tree",
"command.review.toggle": "Toggle review",
"command.terminal.new": "New terminal",
"command.terminal.new.description": "Create a new terminal tab",
@@ -87,9 +91,13 @@ export const dict = {
"dialog.provider.group.popular": "Popular",
"dialog.provider.group.other": "Other",
"dialog.provider.tag.recommended": "Recommended",
"dialog.provider.anthropic.note": "Connect with Claude Pro/Max or API key",
"dialog.provider.openai.note": "Connect with ChatGPT Pro/Plus or API key",
"dialog.provider.copilot.note": "Connect with Copilot or API key",
"dialog.provider.opencode.note": "Curated models including Claude, GPT, Gemini and more",
"dialog.provider.anthropic.note": "Direct access to Claude models, including Pro and Max",
"dialog.provider.copilot.note": "Claude models for coding assistance",
"dialog.provider.openai.note": "GPT models for fast, capable general AI tasks",
"dialog.provider.google.note": "Gemini models for fast, structured responses",
"dialog.provider.openrouter.note": "Access all supported models from one provider",
"dialog.provider.vercel.note": "Unified access to AI models with smart routing",
"dialog.model.select.title": "Select model",
"dialog.model.search.placeholder": "Search models",
@@ -100,7 +108,7 @@ export const dict = {
"dialog.model.unpaid.freeModels.title": "Free models provided by OpenCode",
"dialog.model.unpaid.addMore.title": "Add more models from popular providers",
"dialog.provider.viewAll": "View all providers",
"dialog.provider.viewAll": "Show more providers",
"provider.connect.title": "Connect {{provider}}",
"provider.connect.title.anthropicProMax": "Login with Claude Pro/Max",
@@ -137,6 +145,9 @@ export const dict = {
"provider.connect.toast.connected.title": "{{provider}} connected",
"provider.connect.toast.connected.description": "{{provider}} models are now available to use.",
"provider.disconnect.toast.disconnected.title": "{{provider}} disconnected",
"provider.disconnect.toast.disconnected.description": "{{provider}} models are no longer available.",
"model.tag.free": "Free",
"model.tag.latest": "Latest",
"model.provider.anthropic": "Anthropic",
@@ -155,10 +166,13 @@ export const dict = {
"model.tooltip.context": "Context limit {{limit}}",
"common.search.placeholder": "Search",
"common.goBack": "Go back",
"common.goBack": "Back",
"common.goForward": "Forward",
"common.loading": "Loading",
"common.loading.ellipsis": "...",
"common.cancel": "Cancel",
"common.connect": "Connect",
"common.disconnect": "Disconnect",
"common.submit": "Submit",
"common.save": "Save",
"common.saving": "Saving...",
@@ -167,6 +181,8 @@ export const dict = {
"prompt.placeholder.shell": "Enter shell command...",
"prompt.placeholder.normal": 'Ask anything... "{{example}}"',
"prompt.placeholder.summarizeComments": "Summarize comments…",
"prompt.placeholder.summarizeComment": "Summarize comment…",
"prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "esc to exit",
@@ -308,19 +324,20 @@ export const dict = {
"context.usage.view": "View context usage",
"language.en": "English",
"language.zh": "Chinese (Simplified)",
"language.zht": "Chinese (Traditional)",
"language.ko": "Korean",
"language.de": "German",
"language.es": "Spanish",
"language.fr": "French",
"language.ja": "Japanese",
"language.da": "Danish",
"language.ru": "Russian",
"language.pl": "Polish",
"language.ar": "Arabic",
"language.no": "Norwegian",
"language.br": "Portuguese (Brazil)",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Language",
"toast.language.description": "Switched to {{language}}",
@@ -337,6 +354,10 @@ export const dict = {
"toast.model.none.description": "Connect a provider to summarize this session",
"toast.file.loadFailed.title": "Failed to load file",
"toast.file.listFailed.title": "Failed to list files",
"toast.context.noLineSelection.title": "No line selection",
"toast.context.noLineSelection.description": "Select a line range in a file tab first.",
"toast.session.share.copyFailed.title": "Failed to copy URL to clipboard",
"toast.session.share.success.title": "Session shared",
@@ -412,8 +433,15 @@ export const dict = {
"session.tab.context": "Context",
"session.panel.reviewAndFiles": "Review and files",
"session.review.filesChanged": "{{count}} Files Changed",
"session.review.change.one": "Change",
"session.review.change.other": "Changes",
"session.review.loadingChanges": "Loading changes...",
"session.review.empty": "No changes in this session yet",
"session.review.noChanges": "No changes",
"session.files.selectToOpen": "Select a file to open",
"session.files.all": "All files",
"session.messages.renderEarlier": "Render earlier messages",
"session.messages.loadingEarlier": "Loading earlier messages...",
"session.messages.loadEarlier": "Load earlier messages",
@@ -430,6 +458,14 @@ export const dict = {
"session.header.search.placeholder": "Search {{project}}",
"session.header.searchFiles": "Search files",
"status.popover.trigger": "Status",
"status.popover.ariaLabel": "Server configurations",
"status.popover.tab.servers": "Servers",
"status.popover.tab.mcp": "MCP",
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Plugins",
"status.popover.action.manageServers": "Manage servers",
"session.share.popover.title": "Publish on web",
"session.share.popover.description.shared":
"This session is public on the web. It is accessible to anyone with the link.",
@@ -482,12 +518,16 @@ export const dict = {
"sidebar.project.recentSessions": "Recent sessions",
"sidebar.project.viewAllSessions": "View all sessions",
"app.name.desktop": "OpenCode Desktop",
"settings.section.desktop": "Desktop",
"settings.section.server": "Server",
"settings.tab.general": "General",
"settings.tab.shortcuts": "Shortcuts",
"settings.general.section.appearance": "Appearance",
"settings.general.section.notifications": "System notifications",
"settings.general.section.updates": "Updates",
"settings.general.section.sounds": "Sound effects",
"settings.general.row.language.title": "Language",
@@ -498,6 +538,18 @@ export const dict = {
"settings.general.row.theme.description": "Customise how OpenCode is themed.",
"settings.general.row.font.title": "Font",
"settings.general.row.font.description": "Customise the mono font used in code blocks",
"settings.general.row.releaseNotes.title": "Release notes",
"settings.general.row.releaseNotes.description": "Show What's New popups after updates",
"settings.updates.row.startup.title": "Check for updates on startup",
"settings.updates.row.startup.description": "Automatically check for updates when OpenCode launches",
"settings.updates.row.check.title": "Check for updates",
"settings.updates.row.check.description": "Manually check for updates and install if available",
"settings.updates.action.checkNow": "Check now",
"settings.updates.action.checking": "Checking...",
"settings.updates.toast.latest.title": "You're up to date",
"settings.updates.toast.latest.description": "You're running the latest version of OpenCode.",
"font.option.ibmPlexMono": "IBM Plex Mono",
"font.option.cascadiaCode": "Cascadia Code",
"font.option.firaCode": "Fira Code",
@@ -591,6 +643,13 @@ export const dict = {
"settings.providers.title": "Providers",
"settings.providers.description": "Provider settings will be configurable here.",
"settings.providers.section.connected": "Connected providers",
"settings.providers.connected.empty": "No connected providers",
"settings.providers.section.popular": "Popular providers",
"settings.providers.tag.environment": "Environment",
"settings.providers.tag.config": "Config",
"settings.providers.tag.custom": "Custom",
"settings.providers.tag.other": "Other",
"settings.models.title": "Models",
"settings.models.description": "Model settings will be configurable here.",
"settings.agents.title": "Agents",
@@ -657,6 +716,7 @@ export const dict = {
"workspace.reset.failed.title": "Failed to reset workspace",
"workspace.reset.success.title": "Workspace reset",
"workspace.reset.success.description": "Workspace now matches the default branch.",
"workspace.error.stillPreparing": "Workspace is still preparing",
"workspace.status.checking": "Checking for unmerged changes...",
"workspace.status.error": "Unable to verify git status.",
"workspace.status.clean": "No unmerged changes detected.",
+150 -14
View File
@@ -8,6 +8,7 @@ export const dict = {
"command.category.theme": "Tema",
"command.category.language": "Idioma",
"command.category.file": "Archivo",
"command.category.context": "Contexto",
"command.category.terminal": "Terminal",
"command.category.model": "Modelo",
"command.category.mcp": "MCP",
@@ -15,6 +16,7 @@ export const dict = {
"command.category.permissions": "Permisos",
"command.category.workspace": "Espacio de trabajo",
"command.category.settings": "Ajustes",
"theme.scheme.system": "Sistema",
"theme.scheme.light": "Claro",
"theme.scheme.dark": "Oscuro",
@@ -23,6 +25,7 @@ export const dict = {
"command.project.open": "Abrir proyecto",
"command.provider.connect": "Conectar proveedor",
"command.server.switch": "Cambiar servidor",
"command.settings.open": "Abrir ajustes",
"command.session.previous": "Sesión anterior",
"command.session.next": "Siguiente sesión",
"command.session.archive": "Archivar sesión",
@@ -40,7 +43,10 @@ export const dict = {
"command.session.new": "Nueva sesión",
"command.file.open": "Abrir archivo",
"command.file.open.description": "Buscar archivos y comandos",
"command.context.addSelection": "Añadir selección al contexto",
"command.context.addSelection.description": "Añadir las líneas seleccionadas del archivo actual",
"command.terminal.toggle": "Alternar terminal",
"command.fileTree.toggle": "Alternar árbol de archivos",
"command.review.toggle": "Alternar revisión",
"command.terminal.new": "Nueva terminal",
"command.terminal.new.description": "Crear una nueva pestaña de terminal",
@@ -98,7 +104,7 @@ export const dict = {
"dialog.model.unpaid.freeModels.title": "Modelos gratuitos proporcionados por OpenCode",
"dialog.model.unpaid.addMore.title": "Añadir más modelos de proveedores populares",
"dialog.provider.viewAll": "Ver todos los proveedores",
"dialog.provider.viewAll": "Ver s proveedores",
"provider.connect.title": "Conectar {{provider}}",
"provider.connect.title.anthropicProMax": "Iniciar sesión con Claude Pro/Max",
@@ -117,6 +123,7 @@ export const dict = {
"provider.connect.opencodeZen.line2":
"Con una sola clave API obtendrás acceso a modelos como Claude, GPT, Gemini, GLM y más.",
"provider.connect.opencodeZen.visit.prefix": "Visita ",
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
"provider.connect.opencodeZen.visit.suffix": " para obtener tu clave API.",
"provider.connect.oauth.code.visit.prefix": "Visita ",
"provider.connect.oauth.code.visit.link": "este enlace",
@@ -134,13 +141,32 @@ export const dict = {
"provider.connect.toast.connected.title": "{{provider}} conectado",
"provider.connect.toast.connected.description": "Los modelos de {{provider}} ahora están disponibles para usar.",
"provider.disconnect.toast.disconnected.title": "{{provider}} desconectado",
"provider.disconnect.toast.disconnected.description": "Los modelos de {{provider}} ya no están disponibles.",
"model.tag.free": "Gratis",
"model.tag.latest": "Último",
"model.provider.anthropic": "Anthropic",
"model.provider.openai": "OpenAI",
"model.provider.google": "Google",
"model.provider.xai": "xAI",
"model.provider.meta": "Meta",
"model.input.text": "texto",
"model.input.image": "imagen",
"model.input.audio": "audio",
"model.input.video": "video",
"model.input.pdf": "pdf",
"model.tooltip.allows": "Permite: {{inputs}}",
"model.tooltip.reasoning.allowed": "Permite razonamiento",
"model.tooltip.reasoning.none": "Sin razonamiento",
"model.tooltip.context": "Límite de contexto {{limit}}",
"common.search.placeholder": "Buscar",
"common.goBack": "Volver",
"common.loading": "Cargando",
"common.loading.ellipsis": "...",
"common.cancel": "Cancelar",
"common.connect": "Conectar",
"common.disconnect": "Desconectar",
"common.submit": "Enviar",
"common.save": "Guardar",
"common.saving": "Guardando...",
@@ -149,6 +175,8 @@ export const dict = {
"prompt.placeholder.shell": "Introduce comando de shell...",
"prompt.placeholder.normal": 'Pregunta cualquier cosa... "{{example}}"',
"prompt.placeholder.summarizeComments": "Resumir comentarios…",
"prompt.placeholder.summarizeComment": "Resumir comentario…",
"prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "esc para salir",
@@ -252,6 +280,10 @@ export const dict = {
"dialog.project.edit.color": "Color",
"dialog.project.edit.color.select": "Seleccionar color {{color}}",
"dialog.project.edit.worktree.startup": "Script de inicio del espacio de trabajo",
"dialog.project.edit.worktree.startup.description":
"Se ejecuta después de crear un nuevo espacio de trabajo (árbol de trabajo).",
"dialog.project.edit.worktree.startup.placeholder": "p. ej. bun install",
"context.breakdown.title": "Desglose de Contexto",
"context.breakdown.note":
'Desglose aproximado de tokens de entrada. "Otro" incluye definiciones de herramientas y sobrecarga.',
@@ -287,20 +319,21 @@ export const dict = {
"context.usage.clickToView": "Haz clic para ver contexto",
"context.usage.view": "Ver uso del contexto",
"language.en": "Inglés",
"language.zh": "Chino (simplificado)",
"language.zht": "Chino (tradicional)",
"language.ko": "Coreano",
"language.de": "Alemán",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Frans",
"language.ja": "Japonés",
"language.da": "Danés",
"language.ru": "Ruso",
"language.pl": "Polaco",
"language.ar": "Árabe",
"language.no": "Noruego",
"language.br": "Portugués (Brasil)",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Idioma",
"toast.language.description": "Cambiado a {{language}}",
@@ -318,6 +351,9 @@ export const dict = {
"toast.file.loadFailed.title": "Fallo al cargar archivo",
"toast.file.listFailed.title": "Fallo al listar archivos",
"toast.context.noLineSelection.title": "Sin selección de líneas",
"toast.context.noLineSelection.description": "Primero selecciona un rango de líneas en una pestaña de archivo.",
"toast.session.share.copyFailed.title": "Fallo al copiar URL al portapapeles",
"toast.session.share.success.title": "Sesión compartida",
"toast.session.share.success.description": "¡URL compartida copiada al portapapeles!",
@@ -393,13 +429,19 @@ export const dict = {
"session.tab.context": "Contexto",
"session.panel.reviewAndFiles": "Revisión y archivos",
"session.review.filesChanged": "{{count}} Archivos Cambiados",
"session.review.change.one": "Cambio",
"session.review.change.other": "Cambios",
"session.review.loadingChanges": "Cargando cambios...",
"session.review.empty": "No hay cambios en esta sesión aún",
"session.review.noChanges": "Sin cambios",
"session.files.selectToOpen": "Selecciona un archivo para abrir",
"session.files.all": "Todos los archivos",
"session.messages.renderEarlier": "Renderizar mensajes anteriores",
"session.messages.loadingEarlier": "Cargando mensajes anteriores...",
"session.messages.loadEarlier": "Cargar mensajes anteriores",
"session.messages.loading": "Cargando mensajes...",
"session.messages.jumpToLatest": "Ir al último",
"session.context.addToContext": "Añadir {{selection}} al contexto",
"session.new.worktree.main": "Rama principal",
@@ -410,6 +452,14 @@ export const dict = {
"session.header.search.placeholder": "Buscar {{project}}",
"session.header.searchFiles": "Buscar archivos",
"status.popover.trigger": "Estado",
"status.popover.ariaLabel": "Configuraciones del servidor",
"status.popover.tab.servers": "Servidores",
"status.popover.tab.mcp": "MCP",
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Plugins",
"status.popover.action.manageServers": "Administrar servidores",
"session.share.popover.title": "Publicar en web",
"session.share.popover.description.shared":
"Esta sesión es pública en la web. Es accesible para cualquiera con el enlace.",
@@ -433,6 +483,9 @@ export const dict = {
"terminal.title.numbered": "Terminal {{number}}",
"terminal.close": "Cerrar terminal",
"terminal.connectionLost.title": "Conexión perdida",
"terminal.connectionLost.description":
"La conexión del terminal se interrumpió. Esto puede ocurrir cuando el servidor se reinicia.",
"common.closeTab": "Cerrar pestaña",
"common.dismiss": "Descartar",
"common.requestFailed": "Solicitud fallida",
@@ -446,6 +499,8 @@ export const dict = {
"common.edit": "Editar",
"common.loadMore": "Cargar más",
"common.key.esc": "ESC",
"sidebar.menu.toggle": "Alternar menú",
"sidebar.nav.projectsAndSessions": "Proyectos y sesiones",
"sidebar.settings": "Ajustes",
"sidebar.help": "Ayuda",
@@ -457,12 +512,15 @@ export const dict = {
"sidebar.project.recentSessions": "Sesiones recientes",
"sidebar.project.viewAllSessions": "Ver todas las sesiones",
"app.name.desktop": "OpenCode Desktop",
"settings.section.desktop": "Escritorio",
"settings.section.server": "Servidor",
"settings.tab.general": "General",
"settings.tab.shortcuts": "Atajos",
"settings.general.section.appearance": "Apariencia",
"settings.general.section.notifications": "Notificaciones del sistema",
"settings.general.section.updates": "Actualizaciones",
"settings.general.section.sounds": "Efectos de sonido",
"settings.general.row.language.title": "Idioma",
@@ -474,6 +532,76 @@ export const dict = {
"settings.general.row.font.title": "Fuente",
"settings.general.row.font.description": "Personaliza la fuente mono usada en bloques de código",
"settings.general.row.releaseNotes.title": "Notas de la versión",
"settings.general.row.releaseNotes.description":
'Mostrar ventanas emergentes de "Novedades" después de las actualizaciones',
"settings.updates.row.startup.title": "Buscar actualizaciones al iniciar",
"settings.updates.row.startup.description": "Buscar actualizaciones automáticamente cuando se inicia OpenCode",
"settings.updates.row.check.title": "Buscar actualizaciones",
"settings.updates.row.check.description": "Buscar actualizaciones manualmente e instalarlas si hay alguna",
"settings.updates.action.checkNow": "Buscar ahora",
"settings.updates.action.checking": "Buscando...",
"settings.updates.toast.latest.title": "Estás al día",
"settings.updates.toast.latest.description": "Estás usando la última versión de OpenCode.",
"font.option.ibmPlexMono": "IBM Plex Mono",
"font.option.cascadiaCode": "Cascadia Code",
"font.option.firaCode": "Fira Code",
"font.option.hack": "Hack",
"font.option.inconsolata": "Inconsolata",
"font.option.intelOneMono": "Intel One Mono",
"font.option.iosevka": "Iosevka",
"font.option.jetbrainsMono": "JetBrains Mono",
"font.option.mesloLgs": "Meslo LGS",
"font.option.robotoMono": "Roboto Mono",
"font.option.sourceCodePro": "Source Code Pro",
"font.option.ubuntuMono": "Ubuntu Mono",
"sound.option.alert01": "Alerta 01",
"sound.option.alert02": "Alerta 02",
"sound.option.alert03": "Alerta 03",
"sound.option.alert04": "Alerta 04",
"sound.option.alert05": "Alerta 05",
"sound.option.alert06": "Alerta 06",
"sound.option.alert07": "Alerta 07",
"sound.option.alert08": "Alerta 08",
"sound.option.alert09": "Alerta 09",
"sound.option.alert10": "Alerta 10",
"sound.option.bipbop01": "Bip-bop 01",
"sound.option.bipbop02": "Bip-bop 02",
"sound.option.bipbop03": "Bip-bop 03",
"sound.option.bipbop04": "Bip-bop 04",
"sound.option.bipbop05": "Bip-bop 05",
"sound.option.bipbop06": "Bip-bop 06",
"sound.option.bipbop07": "Bip-bop 07",
"sound.option.bipbop08": "Bip-bop 08",
"sound.option.bipbop09": "Bip-bop 09",
"sound.option.bipbop10": "Bip-bop 10",
"sound.option.staplebops01": "Staplebops 01",
"sound.option.staplebops02": "Staplebops 02",
"sound.option.staplebops03": "Staplebops 03",
"sound.option.staplebops04": "Staplebops 04",
"sound.option.staplebops05": "Staplebops 05",
"sound.option.staplebops06": "Staplebops 06",
"sound.option.staplebops07": "Staplebops 07",
"sound.option.nope01": "No 01",
"sound.option.nope02": "No 02",
"sound.option.nope03": "No 03",
"sound.option.nope04": "No 04",
"sound.option.nope05": "No 05",
"sound.option.nope06": "No 06",
"sound.option.nope07": "No 07",
"sound.option.nope08": "No 08",
"sound.option.nope09": "No 09",
"sound.option.nope10": "No 10",
"sound.option.nope11": "No 11",
"sound.option.nope12": "No 12",
"sound.option.yup01": "Sí 01",
"sound.option.yup02": "Sí 02",
"sound.option.yup03": "Sí 03",
"sound.option.yup04": "Sí 04",
"sound.option.yup05": "Sí 05",
"sound.option.yup06": "Sí 06",
"settings.general.notifications.agent.title": "Agente",
"settings.general.notifications.agent.description":
"Mostrar notificación del sistema cuando el agente termine o necesite atención",
@@ -511,6 +639,13 @@ export const dict = {
"settings.providers.title": "Proveedores",
"settings.providers.description": "La configuración de proveedores estará disponible aquí.",
"settings.providers.section.connected": "Proveedores conectados",
"settings.providers.connected.empty": "No hay proveedores conectados",
"settings.providers.section.popular": "Proveedores populares",
"settings.providers.tag.environment": "Entorno",
"settings.providers.tag.config": "Configuración",
"settings.providers.tag.custom": "Personalizado",
"settings.providers.tag.other": "Otro",
"settings.models.title": "Modelos",
"settings.models.description": "La configuración de modelos estará disponible aquí.",
"settings.agents.title": "Agentes",
@@ -578,6 +713,7 @@ export const dict = {
"workspace.reset.failed.title": "Fallo al restablecer espacio de trabajo",
"workspace.reset.success.title": "Espacio de trabajo restablecido",
"workspace.reset.success.description": "El espacio de trabajo ahora coincide con la rama predeterminada.",
"workspace.error.stillPreparing": "El espacio de trabajo aún se está preparando",
"workspace.status.checking": "Comprobando cambios no fusionados...",
"workspace.status.error": "No se pudo verificar el estado de git.",
"workspace.status.clean": "No se detectaron cambios no fusionados.",
+149 -14
View File
@@ -8,6 +8,7 @@ export const dict = {
"command.category.theme": "Thème",
"command.category.language": "Langue",
"command.category.file": "Fichier",
"command.category.context": "Contexte",
"command.category.terminal": "Terminal",
"command.category.model": "Modèle",
"command.category.mcp": "MCP",
@@ -15,6 +16,7 @@ export const dict = {
"command.category.permissions": "Permissions",
"command.category.workspace": "Espace de travail",
"command.category.settings": "Paramètres",
"theme.scheme.system": "Système",
"theme.scheme.light": "Clair",
"theme.scheme.dark": "Sombre",
@@ -23,6 +25,7 @@ export const dict = {
"command.project.open": "Ouvrir un projet",
"command.provider.connect": "Connecter un fournisseur",
"command.server.switch": "Changer de serveur",
"command.settings.open": "Ouvrir les paramètres",
"command.session.previous": "Session précédente",
"command.session.next": "Session suivante",
"command.session.archive": "Archiver la session",
@@ -40,7 +43,10 @@ export const dict = {
"command.session.new": "Nouvelle session",
"command.file.open": "Ouvrir un fichier",
"command.file.open.description": "Rechercher des fichiers et des commandes",
"command.context.addSelection": "Ajouter la sélection au contexte",
"command.context.addSelection.description": "Ajouter les lignes sélectionnées du fichier actuel",
"command.terminal.toggle": "Basculer le terminal",
"command.fileTree.toggle": "Basculer l'arborescence des fichiers",
"command.review.toggle": "Basculer la revue",
"command.terminal.new": "Nouveau terminal",
"command.terminal.new.description": "Créer un nouvel onglet de terminal",
@@ -98,7 +104,7 @@ export const dict = {
"dialog.model.unpaid.freeModels.title": "Modèles gratuits fournis par OpenCode",
"dialog.model.unpaid.addMore.title": "Ajouter plus de modèles de fournisseurs populaires",
"dialog.provider.viewAll": "Voir tous les fournisseurs",
"dialog.provider.viewAll": "Voir plus de fournisseurs",
"provider.connect.title": "Connecter {{provider}}",
"provider.connect.title.anthropicProMax": "Connexion avec Claude Pro/Max",
@@ -117,6 +123,7 @@ export const dict = {
"provider.connect.opencodeZen.line2":
"Avec une seule clé API, vous aurez accès à des modèles tels que Claude, GPT, Gemini, GLM et plus encore.",
"provider.connect.opencodeZen.visit.prefix": "Visitez ",
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
"provider.connect.opencodeZen.visit.suffix": " pour récupérer votre clé API.",
"provider.connect.oauth.code.visit.prefix": "Visitez ",
"provider.connect.oauth.code.visit.link": "ce lien",
@@ -134,13 +141,32 @@ export const dict = {
"provider.connect.toast.connected.title": "{{provider}} connecté",
"provider.connect.toast.connected.description": "Les modèles {{provider}} sont maintenant disponibles.",
"provider.disconnect.toast.disconnected.title": "{{provider}} déconnecté",
"provider.disconnect.toast.disconnected.description": "Les modèles {{provider}} ne sont plus disponibles.",
"model.tag.free": "Gratuit",
"model.tag.latest": "Dernier",
"model.provider.anthropic": "Anthropic",
"model.provider.openai": "OpenAI",
"model.provider.google": "Google",
"model.provider.xai": "xAI",
"model.provider.meta": "Meta",
"model.input.text": "texte",
"model.input.image": "image",
"model.input.audio": "audio",
"model.input.video": "vidéo",
"model.input.pdf": "pdf",
"model.tooltip.allows": "Autorise : {{inputs}}",
"model.tooltip.reasoning.allowed": "Autorise le raisonnement",
"model.tooltip.reasoning.none": "Sans raisonnement",
"model.tooltip.context": "Limite de contexte {{limit}}",
"common.search.placeholder": "Rechercher",
"common.goBack": "Retour",
"common.loading": "Chargement",
"common.loading.ellipsis": "...",
"common.cancel": "Annuler",
"common.connect": "Connecter",
"common.disconnect": "Déconnecter",
"common.submit": "Soumettre",
"common.save": "Enregistrer",
"common.saving": "Enregistrement...",
@@ -149,6 +175,8 @@ export const dict = {
"prompt.placeholder.shell": "Entrez une commande shell...",
"prompt.placeholder.normal": 'Demandez n\'importe quoi... "{{example}}"',
"prompt.placeholder.summarizeComments": "Résumer les commentaires…",
"prompt.placeholder.summarizeComment": "Résumer le commentaire…",
"prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "esc pour quitter",
@@ -252,6 +280,10 @@ export const dict = {
"dialog.project.edit.color": "Couleur",
"dialog.project.edit.color.select": "Sélectionner la couleur {{color}}",
"dialog.project.edit.worktree.startup": "Script de démarrage de l'espace de travail",
"dialog.project.edit.worktree.startup.description":
"S'exécute après la création d'un nouvel espace de travail (arbre de travail).",
"dialog.project.edit.worktree.startup.placeholder": "p. ex. bun install",
"context.breakdown.title": "Répartition du contexte",
"context.breakdown.note":
"Répartition approximative des jetons d'entrée. \"Autre\" inclut les définitions d'outils et les frais généraux.",
@@ -287,20 +319,21 @@ export const dict = {
"context.usage.clickToView": "Cliquez pour voir le contexte",
"context.usage.view": "Voir l'utilisation du contexte",
"language.en": "Anglais",
"language.zh": "Chinois (simplifié)",
"language.zht": "Chinois (traditionnel)",
"language.ko": "Coréen",
"language.de": "Allemand",
"language.es": "Espagnol",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.ja": "Japonais",
"language.da": "Danois",
"language.ru": "Russe",
"language.pl": "Polonais",
"language.ar": "Arabe",
"language.no": "Norvégien",
"language.br": "Portugais (Brésil)",
"language.da": "Dansk",
"language.ja": "日本語",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Portugs (Brasil)",
"language.th": "ไทย",
"toast.language.title": "Langue",
"toast.language.description": "Passé à {{language}}",
@@ -320,6 +353,9 @@ export const dict = {
"toast.file.loadFailed.title": "Échec du chargement du fichier",
"toast.file.listFailed.title": "Échec de la liste des fichiers",
"toast.context.noLineSelection.title": "Aucune sélection de lignes",
"toast.context.noLineSelection.description": "Sélectionnez d'abord une plage de lignes dans un onglet de fichier.",
"toast.session.share.copyFailed.title": "Échec de la copie de l'URL dans le presse-papiers",
"toast.session.share.success.title": "Session partagée",
"toast.session.share.success.description": "URL de partage copiée dans le presse-papiers !",
@@ -398,13 +434,19 @@ export const dict = {
"session.tab.context": "Contexte",
"session.panel.reviewAndFiles": "Revue et fichiers",
"session.review.filesChanged": "{{count}} fichiers modifiés",
"session.review.change.one": "Modification",
"session.review.change.other": "Modifications",
"session.review.loadingChanges": "Chargement des modifications...",
"session.review.empty": "Aucune modification dans cette session pour l'instant",
"session.review.noChanges": "Aucune modification",
"session.files.selectToOpen": "Sélectionnez un fichier à ouvrir",
"session.files.all": "Tous les fichiers",
"session.messages.renderEarlier": "Afficher les messages précédents",
"session.messages.loadingEarlier": "Chargement des messages précédents...",
"session.messages.loadEarlier": "Charger les messages précédents",
"session.messages.loading": "Chargement des messages...",
"session.messages.jumpToLatest": "Aller au dernier",
"session.context.addToContext": "Ajouter {{selection}} au contexte",
"session.new.worktree.main": "Branche principale",
@@ -415,6 +457,14 @@ export const dict = {
"session.header.search.placeholder": "Rechercher {{project}}",
"session.header.searchFiles": "Rechercher des fichiers",
"status.popover.trigger": "Statut",
"status.popover.ariaLabel": "Configurations des serveurs",
"status.popover.tab.servers": "Serveurs",
"status.popover.tab.mcp": "MCP",
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "Plugins",
"status.popover.action.manageServers": "Gérer les serveurs",
"session.share.popover.title": "Publier sur le web",
"session.share.popover.description.shared":
"Cette session est publique sur le web. Elle est accessible à toute personne disposant du lien.",
@@ -438,6 +488,9 @@ export const dict = {
"terminal.title.numbered": "Terminal {{number}}",
"terminal.close": "Fermer le terminal",
"terminal.connectionLost.title": "Connexion perdue",
"terminal.connectionLost.description":
"La connexion au terminal a été interrompue. Cela peut arriver lorsque le serveur redémarre.",
"common.closeTab": "Fermer l'onglet",
"common.dismiss": "Ignorer",
"common.requestFailed": "La demande a échoué",
@@ -451,6 +504,8 @@ export const dict = {
"common.edit": "Modifier",
"common.loadMore": "Charger plus",
"common.key.esc": "ESC",
"sidebar.menu.toggle": "Basculer le menu",
"sidebar.nav.projectsAndSessions": "Projets et sessions",
"sidebar.settings": "Paramètres",
"sidebar.help": "Aide",
@@ -464,12 +519,15 @@ export const dict = {
"sidebar.project.recentSessions": "Sessions récentes",
"sidebar.project.viewAllSessions": "Voir toutes les sessions",
"app.name.desktop": "OpenCode Desktop",
"settings.section.desktop": "Bureau",
"settings.section.server": "Serveur",
"settings.tab.general": "Général",
"settings.tab.shortcuts": "Raccourcis",
"settings.general.section.appearance": "Apparence",
"settings.general.section.notifications": "Notifications système",
"settings.general.section.updates": "Mises à jour",
"settings.general.section.sounds": "Effets sonores",
"settings.general.row.language.title": "Langue",
@@ -481,6 +539,75 @@ export const dict = {
"settings.general.row.font.title": "Police",
"settings.general.row.font.description": "Personnaliser la police mono utilisée dans les blocs de code",
"settings.general.row.releaseNotes.title": "Notes de version",
"settings.general.row.releaseNotes.description": 'Afficher des pop-ups "Quoi de neuf" après les mises à jour',
"settings.updates.row.startup.title": "Vérifier les mises à jour au démarrage",
"settings.updates.row.startup.description": "Vérifier automatiquement les mises à jour au lancement d'OpenCode",
"settings.updates.row.check.title": "Vérifier les mises à jour",
"settings.updates.row.check.description": "Vérifier manuellement les mises à jour et installer si disponible",
"settings.updates.action.checkNow": "Vérifier maintenant",
"settings.updates.action.checking": "Vérification...",
"settings.updates.toast.latest.title": "Vous êtes à jour",
"settings.updates.toast.latest.description": "Vous utilisez la dernière version d'OpenCode.",
"font.option.ibmPlexMono": "IBM Plex Mono",
"font.option.cascadiaCode": "Cascadia Code",
"font.option.firaCode": "Fira Code",
"font.option.hack": "Hack",
"font.option.inconsolata": "Inconsolata",
"font.option.intelOneMono": "Intel One Mono",
"font.option.iosevka": "Iosevka",
"font.option.jetbrainsMono": "JetBrains Mono",
"font.option.mesloLgs": "Meslo LGS",
"font.option.robotoMono": "Roboto Mono",
"font.option.sourceCodePro": "Source Code Pro",
"font.option.ubuntuMono": "Ubuntu Mono",
"sound.option.alert01": "Alerte 01",
"sound.option.alert02": "Alerte 02",
"sound.option.alert03": "Alerte 03",
"sound.option.alert04": "Alerte 04",
"sound.option.alert05": "Alerte 05",
"sound.option.alert06": "Alerte 06",
"sound.option.alert07": "Alerte 07",
"sound.option.alert08": "Alerte 08",
"sound.option.alert09": "Alerte 09",
"sound.option.alert10": "Alerte 10",
"sound.option.bipbop01": "Bip-bop 01",
"sound.option.bipbop02": "Bip-bop 02",
"sound.option.bipbop03": "Bip-bop 03",
"sound.option.bipbop04": "Bip-bop 04",
"sound.option.bipbop05": "Bip-bop 05",
"sound.option.bipbop06": "Bip-bop 06",
"sound.option.bipbop07": "Bip-bop 07",
"sound.option.bipbop08": "Bip-bop 08",
"sound.option.bipbop09": "Bip-bop 09",
"sound.option.bipbop10": "Bip-bop 10",
"sound.option.staplebops01": "Staplebops 01",
"sound.option.staplebops02": "Staplebops 02",
"sound.option.staplebops03": "Staplebops 03",
"sound.option.staplebops04": "Staplebops 04",
"sound.option.staplebops05": "Staplebops 05",
"sound.option.staplebops06": "Staplebops 06",
"sound.option.staplebops07": "Staplebops 07",
"sound.option.nope01": "Non 01",
"sound.option.nope02": "Non 02",
"sound.option.nope03": "Non 03",
"sound.option.nope04": "Non 04",
"sound.option.nope05": "Non 05",
"sound.option.nope06": "Non 06",
"sound.option.nope07": "Non 07",
"sound.option.nope08": "Non 08",
"sound.option.nope09": "Non 09",
"sound.option.nope10": "Non 10",
"sound.option.nope11": "Non 11",
"sound.option.nope12": "Non 12",
"sound.option.yup01": "Oui 01",
"sound.option.yup02": "Oui 02",
"sound.option.yup03": "Oui 03",
"sound.option.yup04": "Oui 04",
"sound.option.yup05": "Oui 05",
"sound.option.yup06": "Oui 06",
"settings.general.notifications.agent.title": "Agent",
"settings.general.notifications.agent.description":
"Afficher une notification système lorsque l'agent a terminé ou nécessite une attention",
@@ -517,6 +644,13 @@ export const dict = {
"settings.providers.title": "Fournisseurs",
"settings.providers.description": "Les paramètres des fournisseurs seront configurables ici.",
"settings.providers.section.connected": "Fournisseurs connectés",
"settings.providers.connected.empty": "Aucun fournisseur connecté",
"settings.providers.section.popular": "Fournisseurs populaires",
"settings.providers.tag.environment": "Environnement",
"settings.providers.tag.config": "Configuration",
"settings.providers.tag.custom": "Personnalisé",
"settings.providers.tag.other": "Autre",
"settings.models.title": "Modèles",
"settings.models.description": "Les paramètres des modèles seront configurables ici.",
"settings.agents.title": "Agents",
@@ -585,6 +719,7 @@ export const dict = {
"workspace.reset.failed.title": "Échec de la réinitialisation de l'espace de travail",
"workspace.reset.success.title": "Espace de travail réinitialisé",
"workspace.reset.success.description": "L'espace de travail correspond maintenant à la branche par défaut.",
"workspace.error.stillPreparing": "L'espace de travail est encore en cours de préparation",
"workspace.status.checking": "Vérification des modifications non fusionnées...",
"workspace.status.error": "Impossible de vérifier le statut git.",
"workspace.status.clean": "Aucune modification non fusionnée détectée.",
+149 -14
View File
@@ -8,6 +8,7 @@ export const dict = {
"command.category.theme": "テーマ",
"command.category.language": "言語",
"command.category.file": "ファイル",
"command.category.context": "コンテキスト",
"command.category.terminal": "ターミナル",
"command.category.model": "モデル",
"command.category.mcp": "MCP",
@@ -15,6 +16,7 @@ export const dict = {
"command.category.permissions": "権限",
"command.category.workspace": "ワークスペース",
"command.category.settings": "設定",
"theme.scheme.system": "システム",
"theme.scheme.light": "ライト",
"theme.scheme.dark": "ダーク",
@@ -23,6 +25,7 @@ export const dict = {
"command.project.open": "プロジェクトを開く",
"command.provider.connect": "プロバイダーに接続",
"command.server.switch": "サーバーの切り替え",
"command.settings.open": "設定を開く",
"command.session.previous": "前のセッション",
"command.session.next": "次のセッション",
"command.session.archive": "セッションをアーカイブ",
@@ -40,7 +43,10 @@ export const dict = {
"command.session.new": "新しいセッション",
"command.file.open": "ファイルを開く",
"command.file.open.description": "ファイルとコマンドを検索",
"command.context.addSelection": "選択範囲をコンテキストに追加",
"command.context.addSelection.description": "現在のファイルから選択した行を追加",
"command.terminal.toggle": "ターミナルの切り替え",
"command.fileTree.toggle": "ファイルツリーを切り替え",
"command.review.toggle": "レビューの切り替え",
"command.terminal.new": "新しいターミナル",
"command.terminal.new.description": "新しいターミナルタブを作成",
@@ -98,7 +104,7 @@ export const dict = {
"dialog.model.unpaid.freeModels.title": "OpenCodeが提供する無料モデル",
"dialog.model.unpaid.addMore.title": "人気のプロバイダーからモデルを追加",
"dialog.provider.viewAll": "すべてのプロバイダーを表示",
"dialog.provider.viewAll": "さらにプロバイダーを表示",
"provider.connect.title": "{{provider}}を接続",
"provider.connect.title.anthropicProMax": "Claude Pro/Maxでログイン",
@@ -116,6 +122,7 @@ export const dict = {
"OpenCode Zenは、コーディングエージェント向けに最適化された信頼性の高いモデルへのアクセスを提供します。",
"provider.connect.opencodeZen.line2": "1つのAPIキーで、Claude、GPT、Gemini、GLMなどのモデルにアクセスできます。",
"provider.connect.opencodeZen.visit.prefix": " ",
"provider.connect.opencodeZen.visit.link": "opencode.ai/zen",
"provider.connect.opencodeZen.visit.suffix": " にアクセスしてAPIキーを取得してください。",
"provider.connect.oauth.code.visit.prefix": " ",
"provider.connect.oauth.code.visit.link": "このリンク",
@@ -133,13 +140,32 @@ export const dict = {
"provider.connect.toast.connected.title": "{{provider}}が接続されました",
"provider.connect.toast.connected.description": "{{provider}}モデルが使用可能になりました。",
"provider.disconnect.toast.disconnected.title": "{{provider}}が切断されました",
"provider.disconnect.toast.disconnected.description": "{{provider}}のモデルは利用できなくなりました。",
"model.tag.free": "無料",
"model.tag.latest": "最新",
"model.provider.anthropic": "Anthropic",
"model.provider.openai": "OpenAI",
"model.provider.google": "Google",
"model.provider.xai": "xAI",
"model.provider.meta": "Meta",
"model.input.text": "テキスト",
"model.input.image": "画像",
"model.input.audio": "音声",
"model.input.video": "動画",
"model.input.pdf": "pdf",
"model.tooltip.allows": "対応: {{inputs}}",
"model.tooltip.reasoning.allowed": "推論を許可",
"model.tooltip.reasoning.none": "推論なし",
"model.tooltip.context": "コンテキスト上限 {{limit}}",
"common.search.placeholder": "検索",
"common.goBack": "戻る",
"common.loading": "読み込み中",
"common.loading.ellipsis": "...",
"common.cancel": "キャンセル",
"common.connect": "接続",
"common.disconnect": "切断",
"common.submit": "送信",
"common.save": "保存",
"common.saving": "保存中...",
@@ -148,6 +174,8 @@ export const dict = {
"prompt.placeholder.shell": "シェルコマンドを入力...",
"prompt.placeholder.normal": '何でも聞いてください... "{{example}}"',
"prompt.placeholder.summarizeComments": "コメントを要約…",
"prompt.placeholder.summarizeComment": "コメントを要約…",
"prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "escで終了",
@@ -251,6 +279,10 @@ export const dict = {
"dialog.project.edit.color": "色",
"dialog.project.edit.color.select": "{{color}}の色を選択",
"dialog.project.edit.worktree.startup": "ワークスペース起動スクリプト",
"dialog.project.edit.worktree.startup.description":
"新しいワークスペース (ワークツリー) を作成した後に実行されます。",
"dialog.project.edit.worktree.startup.placeholder": "例: bun install",
"context.breakdown.title": "コンテキストの内訳",
"context.breakdown.note": '入力トークンのおおよその内訳です。"その他"にはツールの定義やオーバーヘッドが含まれます。',
"context.breakdown.system": "システム",
@@ -285,20 +317,21 @@ export const dict = {
"context.usage.clickToView": "クリックしてコンテキストを表示",
"context.usage.view": "コンテキスト使用量を表示",
"language.en": "英語",
"language.zh": "中国語(簡体字)",
"language.zht": "中国語(繁体字)",
"language.ko": "韓国語",
"language.de": "ドイツ語",
"language.es": "スペイン語",
"language.fr": "フランス語",
"language.en": "English",
"language.zh": "简体中文",
"language.zht": "繁體中文",
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
"language.da": "デンマーク語",
"language.ru": "ロシア語",
"language.pl": "ポーランド語",
"language.ar": "アラビア語",
"language.no": "ノルウェー語",
"language.br": "ポルトガル語(ブラジル)",
"language.pl": "Polski",
"language.ru": "Русский",
"language.ar": "العربية",
"language.no": "Norsk",
"language.br": "Português (Brasil)",
"language.th": "ไทย",
"toast.language.title": "言語",
"toast.language.description": "{{language}}に切り替えました",
@@ -316,6 +349,9 @@ export const dict = {
"toast.file.loadFailed.title": "ファイルの読み込みに失敗しました",
"toast.file.listFailed.title": "ファイル一覧の取得に失敗しました",
"toast.context.noLineSelection.title": "行が選択されていません",
"toast.context.noLineSelection.description": "まずファイルタブで行範囲を選択してください。",
"toast.session.share.copyFailed.title": "URLのコピーに失敗しました",
"toast.session.share.success.title": "セッションを共有しました",
"toast.session.share.success.description": "共有URLをクリップボードにコピーしました!",
@@ -390,13 +426,19 @@ export const dict = {
"session.tab.context": "コンテキスト",
"session.panel.reviewAndFiles": "レビューとファイル",
"session.review.filesChanged": "{{count}} ファイル変更",
"session.review.change.one": "変更",
"session.review.change.other": "変更",
"session.review.loadingChanges": "変更を読み込み中...",
"session.review.empty": "このセッションでの変更はまだありません",
"session.review.noChanges": "変更なし",
"session.files.selectToOpen": "開くファイルを選択",
"session.files.all": "すべてのファイル",
"session.messages.renderEarlier": "以前のメッセージを表示",
"session.messages.loadingEarlier": "以前のメッセージを読み込み中...",
"session.messages.loadEarlier": "以前のメッセージを読み込む",
"session.messages.loading": "メッセージを読み込み中...",
"session.messages.jumpToLatest": "最新へジャンプ",
"session.context.addToContext": "{{selection}}をコンテキストに追加",
"session.new.worktree.main": "メインブランチ",
@@ -407,6 +449,14 @@ export const dict = {
"session.header.search.placeholder": "{{project}}を検索",
"session.header.searchFiles": "ファイルを検索",
"status.popover.trigger": "ステータス",
"status.popover.ariaLabel": "サーバー設定",
"status.popover.tab.servers": "サーバー",
"status.popover.tab.mcp": "MCP",
"status.popover.tab.lsp": "LSP",
"status.popover.tab.plugins": "プラグイン",
"status.popover.action.manageServers": "サーバーを管理",
"session.share.popover.title": "ウェブで公開",
"session.share.popover.description.shared":
"このセッションはウェブで公開されています。リンクを知っている人なら誰でもアクセスできます。",
@@ -430,6 +480,9 @@ export const dict = {
"terminal.title.numbered": "ターミナル {{number}}",
"terminal.close": "ターミナルを閉じる",
"terminal.connectionLost.title": "接続が失われました",
"terminal.connectionLost.description":
"ターミナルの接続が中断されました。これはサーバーが再起動したときに発生することがあります。",
"common.closeTab": "タブを閉じる",
"common.dismiss": "閉じる",
"common.requestFailed": "リクエスト失敗",
@@ -443,6 +496,8 @@ export const dict = {
"common.edit": "編集",
"common.loadMore": "さらに読み込む",
"common.key.esc": "ESC",
"sidebar.menu.toggle": "メニューを切り替え",
"sidebar.nav.projectsAndSessions": "プロジェクトとセッション",
"sidebar.settings": "設定",
"sidebar.help": "ヘルプ",
@@ -454,12 +509,15 @@ export const dict = {
"sidebar.project.recentSessions": "最近のセッション",
"sidebar.project.viewAllSessions": "すべてのセッションを表示",
"app.name.desktop": "OpenCode Desktop",
"settings.section.desktop": "デスクトップ",
"settings.section.server": "サーバー",
"settings.tab.general": "一般",
"settings.tab.shortcuts": "ショートカット",
"settings.general.section.appearance": "外観",
"settings.general.section.notifications": "システム通知",
"settings.general.section.updates": "アップデート",
"settings.general.section.sounds": "効果音",
"settings.general.row.language.title": "言語",
@@ -471,6 +529,75 @@ export const dict = {
"settings.general.row.font.title": "フォント",
"settings.general.row.font.description": "コードブロックで使用する等幅フォントをカスタマイズします",
"settings.general.row.releaseNotes.title": "リリースノート",
"settings.general.row.releaseNotes.description": "アップデート後に「新機能」ポップアップを表示",
"settings.updates.row.startup.title": "起動時にアップデートを確認",
"settings.updates.row.startup.description": "OpenCode の起動時に自動でアップデートを確認します",
"settings.updates.row.check.title": "アップデートを確認",
"settings.updates.row.check.description": "手動でアップデートを確認し、利用可能ならインストールします",
"settings.updates.action.checkNow": "今すぐ確認",
"settings.updates.action.checking": "確認中...",
"settings.updates.toast.latest.title": "最新です",
"settings.updates.toast.latest.description": "OpenCode は最新バージョンです。",
"font.option.ibmPlexMono": "IBM Plex Mono",
"font.option.cascadiaCode": "Cascadia Code",
"font.option.firaCode": "Fira Code",
"font.option.hack": "Hack",
"font.option.inconsolata": "Inconsolata",
"font.option.intelOneMono": "Intel One Mono",
"font.option.iosevka": "Iosevka",
"font.option.jetbrainsMono": "JetBrains Mono",
"font.option.mesloLgs": "Meslo LGS",
"font.option.robotoMono": "Roboto Mono",
"font.option.sourceCodePro": "Source Code Pro",
"font.option.ubuntuMono": "Ubuntu Mono",
"sound.option.alert01": "アラート 01",
"sound.option.alert02": "アラート 02",
"sound.option.alert03": "アラート 03",
"sound.option.alert04": "アラート 04",
"sound.option.alert05": "アラート 05",
"sound.option.alert06": "アラート 06",
"sound.option.alert07": "アラート 07",
"sound.option.alert08": "アラート 08",
"sound.option.alert09": "アラート 09",
"sound.option.alert10": "アラート 10",
"sound.option.bipbop01": "ビップボップ 01",
"sound.option.bipbop02": "ビップボップ 02",
"sound.option.bipbop03": "ビップボップ 03",
"sound.option.bipbop04": "ビップボップ 04",
"sound.option.bipbop05": "ビップボップ 05",
"sound.option.bipbop06": "ビップボップ 06",
"sound.option.bipbop07": "ビップボップ 07",
"sound.option.bipbop08": "ビップボップ 08",
"sound.option.bipbop09": "ビップボップ 09",
"sound.option.bipbop10": "ビップボップ 10",
"sound.option.staplebops01": "ステープルボップス 01",
"sound.option.staplebops02": "ステープルボップス 02",
"sound.option.staplebops03": "ステープルボップス 03",
"sound.option.staplebops04": "ステープルボップス 04",
"sound.option.staplebops05": "ステープルボップス 05",
"sound.option.staplebops06": "ステープルボップス 06",
"sound.option.staplebops07": "ステープルボップス 07",
"sound.option.nope01": "いいえ 01",
"sound.option.nope02": "いいえ 02",
"sound.option.nope03": "いいえ 03",
"sound.option.nope04": "いいえ 04",
"sound.option.nope05": "いいえ 05",
"sound.option.nope06": "いいえ 06",
"sound.option.nope07": "いいえ 07",
"sound.option.nope08": "いいえ 08",
"sound.option.nope09": "いいえ 09",
"sound.option.nope10": "いいえ 10",
"sound.option.nope11": "いいえ 11",
"sound.option.nope12": "いいえ 12",
"sound.option.yup01": "はい 01",
"sound.option.yup02": "はい 02",
"sound.option.yup03": "はい 03",
"sound.option.yup04": "はい 04",
"sound.option.yup05": "はい 05",
"sound.option.yup06": "はい 06",
"settings.general.notifications.agent.title": "エージェント",
"settings.general.notifications.agent.description":
"エージェントが完了したか、注意が必要な場合にシステム通知を表示します",
@@ -506,6 +633,13 @@ export const dict = {
"settings.providers.title": "プロバイダー",
"settings.providers.description": "プロバイダー設定はここで構成できます。",
"settings.providers.section.connected": "接続済みプロバイダー",
"settings.providers.connected.empty": "接続済みプロバイダーはありません",
"settings.providers.section.popular": "人気のプロバイダー",
"settings.providers.tag.environment": "環境",
"settings.providers.tag.config": "設定",
"settings.providers.tag.custom": "カスタム",
"settings.providers.tag.other": "その他",
"settings.models.title": "モデル",
"settings.models.description": "モデル設定はここで構成できます。",
"settings.agents.title": "エージェント",
@@ -572,6 +706,7 @@ export const dict = {
"workspace.reset.failed.title": "ワークスペースのリセットに失敗しました",
"workspace.reset.success.title": "ワークスペースをリセットしました",
"workspace.reset.success.description": "ワークスペースはデフォルトブランチと一致しています。",
"workspace.error.stillPreparing": "ワークスペースはまだ準備中です",
"workspace.status.checking": "未マージの変更を確認中...",
"workspace.status.error": "gitステータスを確認できません。",
"workspace.status.clean": "未マージの変更は検出されませんでした。",

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