Compare commits

..

1209 Commits

Author SHA1 Message Date
Dax
3c41e4e8f1 chore: rename repo references from sst/opencode to anomalyco/opencode (#6687)
Co-authored-by: Github Action <action@github.com>
2026-01-02 16:02:52 -05:00
Matt Silverlock
66bc046503 fix: merge instructions arrays across config files (#6663) 2026-01-02 13:55:40 -06:00
Matt Silverlock
6e68ea034c fix: handle actions/checkout v6 credential storage change (#6667) 2026-01-02 13:54:43 -06:00
Adam
c51fa7cb24 feat: add /compact session command 2026-01-02 12:40:37 -06:00
Adam
a4c67515c9 fix(desktop): tweak window drag region layout 2026-01-02 12:40:36 -06:00
Adam
1d2d710fce fix: relax request timeouts 2026-01-02 12:40:36 -06:00
Aiden Cline
2fd97377f6 test: fix transform test 2026-01-02 12:38:44 -06:00
Aiden Cline
47ebb2973f test: add message-v2 test 2026-01-02 12:28:40 -06:00
Aiden Cline
49d7ccd1db fix: variant for minimal 2026-01-02 12:28:40 -06:00
Aiden Cline
c996f3d847 chore: ensure empty message isnt sent 2026-01-02 12:28:40 -06:00
Mike English
70881b2937 fix: cloudflare-ai-gateway sdk.chat undefined error (#6407) 2026-01-02 11:24:13 -06:00
rari404
d8753cda02 refactor: use Bun.sleep instead of Promise setTimeout (#6620) 2026-01-02 11:12:02 -06:00
steez
2685de2a33 feat(theme): add Osaka Jade theme (#6609) 2026-01-02 10:26:01 -06:00
Albin Groen
ddb1ec294e fix(ui): fix slight vertical overflow in project selector (#6589) 2026-01-02 06:24:20 -06:00
OpeOginni
fbd9677932 fix(desktop): Properly decode session id for permission context (#6580) 2026-01-02 06:22:50 -06:00
GitHub Action
814e513db7 ignore: update download stats 2026-01-02 2026-01-02 12:04:42 +00:00
Luke Parker
c600114db9 fix(share): handle NotFoundError for non-shared sessions in sync (#6634) 2026-01-02 04:16:12 -06:00
Dax Raad
038cff4a93 core: improve plugin loading to handle builtin plugin failures gracefully 2026-01-01 23:15:04 -05:00
Dax Raad
741cb9c0ef ci 2026-01-01 22:44:22 -05:00
GitHub Action
38e5adc491 chore: generate 2026-01-02 03:12:21 +00:00
Dax Raad
389a5fc017 tui: add reload functionality and improve lazy utility with reset capability 2026-01-01 22:11:43 -05:00
opencode
d60393835c release: v1.0.224 2026-01-02 03:05:57 +00:00
Adam
e6ba241045 wip(app): progress 2026-01-01 21:03:08 -06:00
Adam
cd2c160cf6 fix(app): startup time 2026-01-01 21:03:08 -06:00
Adam
0f34634c52 chore: cleanup 2026-01-01 21:03:07 -06:00
Adam
a5a569f892 chore: cleanup 2026-01-01 21:03:07 -06:00
Adam
afc1825cf5 wip(app): progress 2026-01-01 21:03:06 -06:00
Adam
6b4c433e14 wip(app): progress 2026-01-01 21:03:06 -06:00
Adam
797d8425e0 wip(app): progress 2026-01-01 21:03:06 -06:00
Adam
260eef2d66 wip(app): progress 2026-01-01 21:03:05 -06:00
Adam
93f1e1afb8 wip(desktop): progress 2026-01-01 21:03:05 -06:00
Adam
6acd16dde4 wip(desktop): progress 2026-01-01 21:03:04 -06:00
Adam
6647b1e22f wip(desktop): progress 2026-01-01 21:03:04 -06:00
Adam
b8872d9d20 wip(desktop): progress 2026-01-01 21:03:03 -06:00
Adam
78940d5b7e wip(app): file context 2026-01-01 21:03:03 -06:00
Dax Raad
b84a1f714b tui: remove memory leak fixes documentation after implementation 2026-01-01 21:40:28 -05:00
GitHub Action
07c008fe3d chore: generate 2026-01-02 02:28:48 +00:00
Dax Raad
dad9c917d2 tui: fix memory leaks in session management and improve permission error handling 2026-01-01 21:28:11 -05:00
Dax Raad
2aaea71eb3 tui: add heap snapshot option to system menu for debugging memory usage 2026-01-01 21:18:28 -05:00
Dax Raad
db8d83b53d tui: fix permission tests for new evaluate function signature 2026-01-01 21:01:34 -05:00
Dax Raad
963f407062 tui: improve permission error handling and evaluation logic 2026-01-01 21:01:00 -05:00
Dax Raad
4f1ef93910 ignore 2026-01-01 20:42:06 -05:00
Dillon Mulroy
05eee679a3 feat: add assistant metadata to session export (#6611) 2026-01-01 18:56:23 -06:00
Mani Sundararajan
154c52c4d9 fix: windows fallback for "less" cmd in session list (#6515) 2026-01-01 18:53:29 -06:00
Daniel Polito
680db7b9e4 Desktop: Improve Resize Handle (#6608) 2026-01-01 18:26:34 -06:00
Aiden Cline
7aa1dbe873 test: fix bash test 2026-01-01 17:53:20 -06:00
Aiden Cline
76186d19f3 fix: ensure new permissions changes work for special case bash commands like rm, cd, etc 2026-01-01 17:27:23 -06:00
Aiden Cline
7760b33956 ignore: comment out repo default for ask 2026-01-01 17:13:31 -06:00
GitHub Action
99794c25b0 chore: generate 2026-01-01 22:54:43 +00:00
Dax
351ddeed91 Permission rework (#6319)
Co-authored-by: Github Action <action@github.com>
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
2026-01-01 17:54:11 -05:00
Aiden Cline
dccb8875ad core: fix import command regex 2026-01-01 15:18:32 -06:00
Frank
5f2be55e54 docs: update zen processing fee 2026-01-01 16:10:13 -05:00
alcpereira
8b35d56a48 fix: remove outdated Haiku filter for GitHub Copilot (#6593) 2026-01-01 14:04:31 -06:00
Aiden Cline
9be944a2d2 core: fix stats command day calculation and time filtering 2026-01-01 13:25:26 -06:00
Aiden Cline
5138f9250e ignore: keep the process exit logic 2026-01-01 13:05:08 -06:00
Aiden Cline
e503654252 core: make installdeps non blocking 2026-01-01 13:00:39 -06:00
Aiden Cline
8ebc601ea2 core: use --no-cache when behind proxy to avoid hangs 2026-01-01 12:46:25 -06:00
Saatvik Arya
7a3ff5b98f fix(session): check for context overflow mid-turn in finish-step (#6480) 2026-01-01 12:03:18 -06:00
Lekë Dobruna
3b03324578 fix: display error if invalid agent is used in a command (#6578) 2026-01-01 11:39:21 -06:00
GitHub Action
35fff0ca70 chore: generate 2026-01-01 17:35:37 +00:00
Tom
dc8586371c fix(server): add Content-Type headers for proxied static assets (#6587) 2026-01-01 11:35:04 -06:00
GitHub Action
41f9a58c27 ignore: update download stats 2026-01-01 2026-01-01 12:04:57 +00:00
opencode
01237c5325 release: v1.0.223 2026-01-01 11:25:32 +00:00
Adam
6e7fc30f94 feat(app): context window window 2026-01-01 05:23:07 -06:00
Adam
03733b0505 fix(util): checksum defensiveness 2026-01-01 05:23:07 -06:00
Adam
d1a4295a32 fix(util): checksum defensiveness 2026-01-01 05:23:06 -06:00
Adam
6341ed506c fix(app): update primitive colors 2026-01-01 05:23:06 -06:00
Adam
ed745df375 fix(app): update primitive colors 2026-01-01 05:23:05 -06:00
GitHub Action
80db008419 chore: generate 2026-01-01 04:59:38 +00:00
Aiden Cline
4039670a24 Reapply "fix(tui): don't show 'Agent not found' toast for subagents (#6528)"
This reverts commit 97a0fd1d54.
2025-12-31 22:58:06 -06:00
Github Action
d59357c89b Update Nix flake.lock and hashes 2026-01-01 01:21:05 +00:00
opencode
3331b0600a release: v1.0.222 2026-01-01 01:21:04 +00:00
Luke Parker
c131dd0829 fix(desktop): Windows support for PTY and cross-platform build scripts (#6508) 2025-12-31 19:18:07 -06:00
Adam
1c25f1fae0 feat(desktop): in-app update toasts 2025-12-31 18:19:53 -06:00
Adam
2da71e0a50 feat(desktop): lose the summaries 2025-12-31 18:16:52 -06:00
Daniel Polito
87978b1c17 Desktop: Add Subagents Mention Support (#6540) 2025-12-31 18:07:45 -06:00
GitHub Action
63d2b21b8f chore: generate 2025-12-31 22:16:32 +00:00
Aiden Cline
9a1dc1ffe4 core: prevent TimeoutOverflowWarning by capping setTimeout delay to max 32-bit signed integer 2025-12-31 16:15:49 -06:00
opencode
c93e7621be release: v1.0.221 2025-12-31 21:10:33 +00:00
Adam
e842205550 chore: cleanup 2025-12-31 15:08:00 -06:00
Adam
b2aa387376 feat(app): better model selector 2025-12-31 15:07:59 -06:00
Aiden Cline
34aecda47c tweak: default to ai-sdk/opeai-compatible if no npm package provided 2025-12-31 14:54:21 -06:00
Aiden Cline
b419b0ec55 Reapply "tweak: adjust keys for uniqueness calculations to use provider/model"
This reverts commit 9d32a0354f1db3ea4893f4ad00900c6420ab78c6.
2025-12-31 14:54:21 -06:00
Aiden Cline
538ac208e1 Revert "tweak: adjust keys for uniqueness calculations to use provider/model"
This reverts commit eb81994a18.
2025-12-31 14:54:21 -06:00
Adam
16957fd107 fix(app): auto-accept colors 2025-12-31 14:35:41 -06:00
Adam
7f3a0b8e5c fix(desktop): tooltip colors 2025-12-31 14:20:21 -06:00
Adam
d4a2652eda feat(desktop): better affordance for auto-accept 2025-12-31 14:04:44 -06:00
Adam
7a4bfbe56d fix(app): text selection 2025-12-31 13:32:44 -06:00
Adam
31e2c8b5e9 wip: input changes 2025-12-31 13:31:46 -06:00
Adam
eab23738a8 chore: cleanup 2025-12-31 13:13:50 -06:00
Adam
93845db462 fix(desktop): don't show notifs if auto-accepting 2025-12-31 13:12:31 -06:00
Adam
65bc72098b fix(desktop): more defensive access 2025-12-31 13:12:30 -06:00
Adam
b5546dce80 wip(app): better variant toggle 2025-12-31 13:12:30 -06:00
Adam
3807364e73 chore(app): tool args cleanup 2025-12-31 13:12:29 -06:00
Adam
3a1cfa6c73 chore(app): keybind tooltip component 2025-12-31 13:12:29 -06:00
Adam
a2857bba83 fix(desktop): prompt input cleanup 2025-12-31 13:12:28 -06:00
Aiden Cline
97a0fd1d54 Revert "fix(tui): don't show 'Agent not found' toast for subagents (#6528)"
This reverts commit 87f9ebd17c.
2025-12-31 13:11:12 -06:00
Vlad Temian
87f9ebd17c fix(tui): don't show 'Agent not found' toast for subagents (#6528) 2025-12-31 13:04:23 -06:00
Aiden Cline
e7422ee782 chore: rm unused import 2025-12-31 12:11:42 -06:00
Aiden Cline
dc3e731afe ci: tweak changelog ensure all cmd/ things fall under tui section 2025-12-31 12:05:35 -06:00
GitHub Action
de50e7f9b9 chore: generate 2025-12-31 17:35:47 +00:00
Marcel de Vries
f3db966317 Add support for LSP workspace/didChangeWatchedFiles (#6524)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-31 11:35:09 -06:00
opencode
decc616c80 release: v1.0.220 2025-12-31 17:30:57 +00:00
Aiden Cline
e0450e74fb fix: plugin & mode globs 2025-12-31 11:28:06 -06:00
GitHub Action
094af4dc7d chore: generate 2025-12-31 17:19:02 +00:00
Michael Ramos
2dc14718aa docs: Add plannotator plugin (#6510) 2025-12-31 11:18:28 -06:00
Eric Guo
b97d20f252 feat(cli): New debug agent <name> subcommand (#6529) 2025-12-31 11:16:03 -06:00
Anzul Aqeel
fcfcdd95e9 fix: Clarify agent-name placeholder in tips (#6520) 2025-12-31 11:15:07 -06:00
opencode-agent[bot]
c42bd492ea docs: new configurable CORS option (#6522)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2025-12-31 11:11:20 -06:00
Frank
840fe030ab zen: fix fee instruction 2025-12-31 11:54:01 -05:00
Adam
a7c4f83ca2 fix(desktop): remove status bar, new elements in header 2025-12-31 10:22:17 -06:00
Adam
ed70a07201 fix(desktop): cleanup user message style 2025-12-31 10:22:16 -06:00
Adam
18a5eb205f fix(desktop): better new session button 2025-12-31 10:22:16 -06:00
Paolo Ricciuti
5249f04ea0 fix: display MCP tag for prompts in autocomplete but not in prompt (#6531) 2025-12-31 09:34:36 -06:00
Adam
6e3ead198e fix(ui): radio group styles 2025-12-31 09:26:14 -06:00
GitHub Action
4309d439f5 chore: generate 2025-12-31 15:24:05 +00:00
Adam
2ec6a21cc0 feat(desktop): unified diff toggle 2025-12-31 09:23:24 -06:00
Adam
ebf5ad25c5 fix(desktop): not rendering large sessions 2025-12-31 08:29:36 -06:00
opencode
8aa34ab9f3 release: v1.0.219 2025-12-31 14:01:59 +00:00
Adam
50ef866a02 fix(core): mdns fails if service already registered 2025-12-31 07:33:49 -06:00
Adam
3650fefe2d fix(desktop): don't expand tools by default 2025-12-31 07:10:47 -06:00
GitHub Action
22091c29f1 ignore: update download stats 2025-12-31 2025-12-31 12:04:56 +00:00
Adam
e7e89dc5a6 chore: cleanup 2025-12-31 04:47:24 -06:00
Adam
34e9392bb4 chore: daytona skip preview warning 2025-12-31 04:22:53 -06:00
GitHub Action
05c3bc27ff chore: generate 2025-12-31 09:51:12 +00:00
Adam
b1a6333d17 feat(core): configurable cors hosts 2025-12-31 03:50:29 -06:00
Aiden Cline
5c9d619620 docs: add variants docs (#6516)
Co-authored-by: David Hill <iamdavidhill@gmail.com>
2025-12-31 01:17:50 -06:00
GitHub Action
dfb9caa2a9 chore: generate 2025-12-31 06:51:59 +00:00
Paolo Ricciuti
57a2b5f444 feat: mcp prompts as slash commands (alternative) (#5767)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-31 00:51:25 -06:00
Github Action
977c9a3e2c Update Nix flake.lock and hashes 2025-12-31 06:11:51 +00:00
Qio
db84ee17f4 feat: add gemini-3-flash to fast models list (#6497)
Co-authored-by: qio <handsomehust@gmail.com>
2025-12-31 00:11:47 -06:00
OpeOginni
0b1f6a7d2d feat: bundle in @ai-sdk/vercel version 1.0.31 for aisdk v5 support (#6512) 2025-12-31 00:10:42 -06:00
David Hill
a6d225558c fix: cleaner view subagents hint text (#6437)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-30 22:47:16 -06:00
Brian Clinkenbeard
2434965b7f Update mise install command (#6504) 2025-12-30 22:43:15 -06:00
GitHub Action
d4cf78bceb chore: generate 2025-12-31 04:06:43 +00:00
Dax Raad
ed4ce67cdc core: add configurable timeout for MCP tool calls to prevent hanging requests 2025-12-30 23:06:07 -05:00
Adam
94dca309e9 fix(app): don't open native folder select with remote server 2025-12-30 20:15:57 -06:00
Adam
52e4dd110b feat(app): hide reasoning once agent is done 2025-12-30 20:09:32 -06:00
Adam
1e74560796 feat(app): model variants 2025-12-30 20:06:03 -06:00
Adam
48f2419d9d fix(desktop): better notification icon 2025-12-30 19:40:14 -06:00
Aiden Cline
b9ef09a0f4 tweak: read plurals too and stop erroring on them 2025-12-30 18:58:31 -06:00
Aiden Cline
eb81994a18 tweak: adjust keys for uniqueness calculations to use provider/model 2025-12-30 18:41:28 -06:00
opencode-agent[bot]
a3819e088c docs: for stats --models flag (#6492)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-30 18:31:48 -06:00
Aiden Cline
324ae9c471 fix: openai variants for codex models 2025-12-30 18:18:16 -06:00
processtrader
7349626757 feat: add model usage statistics with input/output token breakdown to stats command (#6296) 2025-12-30 17:52:41 -06:00
Farhad Omid
76c25ef286 feat(format): add rustfmt formatter for Rust files (#6482) 2025-12-30 17:06:55 -06:00
GitHub Action
c8b3b31d27 chore: generate 2025-12-30 22:38:06 +00:00
Aiden Cline
81fef60266 fix: ensure variants also work for completely custom models (#6481)
Co-authored-by: Daniel Smolsky <dannysmo@gmail.com>
2025-12-30 16:37:32 -06:00
GitHub Action
3fe5d91372 chore: generate 2025-12-30 20:32:02 +00:00
Adam
7adb6e495a feat(desktop): upgrade to latest version on error page 2025-12-30 14:31:16 -06:00
opencode
2039c6936f release: v1.0.218 2025-12-30 20:17:26 +00:00
Adam
a02fefe9dc fix(core): cors exception for tauri 2025-12-30 14:14:44 -06:00
Jay V
cb0e05db26 docs: add auto-reload and monthly limits documentation to Zen guide 2025-12-30 12:57:58 -07:00
GitHub Action
b9cdcaa9db chore: generate 2025-12-30 19:14:09 +00:00
ravshansbox
94453eb1bd Add prisma language server (#6462) 2025-12-30 13:13:36 -06:00
Ytzhak
8f629db988 feat: add extract reasoning middleware (#6463) 2025-12-30 13:13:18 -06:00
opencode
585378cba0 release: v1.0.217 2025-12-30 19:01:36 +00:00
Dax Raad
8cd8393339 core: allow CORS requests from tauri://localhost 2025-12-30 13:58:41 -05:00
GitHub Action
b184b2fb73 chore: generate 2025-12-30 18:34:33 +00:00
Aiden Cline
c88c2da9be fix: move variant toggle to command bar 2025-12-30 12:33:50 -06:00
opencode
9b04081ae0 release: v1.0.216 2025-12-30 18:07:37 +00:00
Dax Raad
7d2d87fa2c core: allow CORS requests from *.opencode.ai subdomains 2025-12-30 13:04:18 -05:00
GitHub Action
787f37b382 chore: generate 2025-12-30 17:59:01 +00:00
ja
8fa1af851c style(nix): use idiomatic inherit syntax (#6457) 2025-12-30 11:58:28 -06:00
opencode
73bc3e704e release: v1.0.215 2025-12-30 17:09:08 +00:00
Adam
8d2feed30e fix(desktop): more defensive agent access 2025-12-30 11:03:34 -06:00
GitHub Action
2d8d4e5dee chore: generate 2025-12-30 16:52:42 +00:00
Fayçal Mitidji
b3784588ae Fix: High CPU / memory leak when filtering model list window to empty results (#6435) 2025-12-30 10:52:06 -06:00
opencode
104d52bc38 release: v1.0.214 2025-12-30 16:37:22 +00:00
Adam
dff1fe2d28 fix(desktop): sort servers by health 2025-12-30 10:34:55 -06:00
Adam
72ab4260ee fix(desktop): don't persist fallback server urls 2025-12-30 10:31:48 -06:00
Adam
9e9b4a0555 fix(share): broken share pages 2025-12-30 10:27:06 -06:00
Adam
e53192889c fix(app): better text selection 2025-12-30 10:21:37 -06:00
Rohan Mukherjee
23bbfb3d15 fix: cloudflare provider information (#6426) 2025-12-30 09:45:09 -06:00
opencode-agent[bot]
37da005a01 docs: projects, find.files, notifications (#6438)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2025-12-30 09:44:09 -06:00
Didier Durand
8b708242f1 chore: fix various typos (#6429) 2025-12-30 09:42:04 -06:00
GitHub Action
339d2dcb98 chore: generate 2025-12-30 15:28:59 +00:00
Adam
bbc8678164 fix(desktop): share projects across all local servers 2025-12-30 09:28:19 -06:00
opencode
a1d54475fe release: v1.0.213 2025-12-30 15:26:35 +00:00
Adam
55c601d13a fix(desktop): don't hang on to dead server 2025-12-30 09:24:03 -06:00
Github Action
9115fac4c4 Update Nix flake.lock and hashes 2025-12-30 14:32:38 +00:00
Sebastian Herrlinger
cfcb2c1fd8 upgrade opentui to v0.1.67, fixing split diff alignment and markdown jitter 2025-12-30 15:31:02 +01:00
GitHub Action
221fc62135 chore: generate 2025-12-30 13:37:48 +00:00
opencode
faaef45384 release: v1.0.212 2025-12-30 13:37:47 +00:00
Adam
2d18d80ac3 chore: cleanup 2025-12-30 07:35:20 -06:00
Adam
e0e07c5d48 feat(app): change server 2025-12-30 07:24:40 -06:00
opencode
281f9e6236 release: v1.0.211 2025-12-30 13:04:07 +00:00
Github Action
f88903a901 Update Nix flake.lock and hashes 2025-12-30 12:58:37 +00:00
ryanwyler
ad425a6a6a fix: revert opentui to 0.1.63 to fix streaming jitter regression (#6439) 2025-12-30 13:57:22 +01:00
GitHub Action
e635d37027 chore: generate 2025-12-30 12:40:52 +00:00
Connor Adams
97081484d5 docs: global claude skills (#6436) 2025-12-30 06:40:19 -06:00
GitHub Action
e451504496 ignore: update download stats 2025-12-30 2025-12-30 12:04:47 +00:00
Github Action
53211c5d37 Update Nix flake.lock and hashes 2025-12-30 11:00:29 +00:00
GitHub Action
98b6817e20 chore: generate 2025-12-30 11:00:29 +00:00
opencode
f54d5377a4 release: v1.0.210 2025-12-30 11:00:28 +00:00
Adam
a576fdb5e4 feat(web): open projects 2025-12-30 04:57:37 -06:00
Adam
ae53f876f1 feat(desktop): readline shortcuts 2025-12-30 04:57:36 -06:00
Adam
a7beba5aa9 chore(desktop): disable sourcemap 2025-12-30 04:57:36 -06:00
Adam
e9ef72c20f feat(desktop): more mono (nerd) fonts 2025-12-30 04:57:35 -06:00
Adam
fa1ac7bc95 feat(desktop): system notifications 2025-12-30 04:57:35 -06:00
Aiden Cline
c82ab649e2 ignore: fix bug from variants pr, prevent createEffect issue 2025-12-30 00:14:10 -06:00
Aiden Cline
abc7eed92b tweak: read global claude skills too (#6420) 2025-12-29 23:48:58 -06:00
Joachim Isaksson
1670d220da fix: prevent model list corruption from SolidJS reactivity (#6359)
Co-authored-by: Joachim Isaksson <joachim.isaksson@centiro.com>
2025-12-29 23:45:15 -06:00
Jkker
ddc4e34731 fix(mdns): use named import for bonjour-service (resolves #6422) (#6423) 2025-12-29 23:29:34 -06:00
GitHub Action
af99d83709 chore: generate 2025-12-30 03:44:29 +00:00
Aiden Cline
ed0c0d90be feat: add variants toggle (#6325)
Co-authored-by: Github Action <action@github.com>
2025-12-29 21:43:50 -06:00
Aiden Cline
e1dd9c4ccb ci: improve changelog script 2025-12-29 21:15:36 -06:00
Eduardo Santos de Brito
4657fa823f feat(plugin): expose server URL to plugins (#6373) 2025-12-29 21:05:08 -06:00
opencode-agent[bot]
1d589c7ac7 docs: nix formatter (#6414)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2025-12-29 20:54:25 -06:00
Aiden Cline
6b5a0fb261 ci: update token var 2025-12-29 18:16:17 -06:00
Github Action
6d93a7bf55 Update Nix flake.lock and hashes 2025-12-30 00:15:46 +00:00
Sebastian Herrlinger
4ca7ab6be8 upgrade opentui to v0.1.66, fixing split diff alignment 2025-12-30 01:14:18 +01:00
Silvio Ney
713d996b9f fix: Correct theme command in tui.mdx (#6410) 2025-12-29 18:11:07 -06:00
Aiden Cline
aa16610021 ci: fix env 2025-12-29 17:52:36 -06:00
GitHub Action
d98568fe7e chore: generate 2025-12-29 23:47:03 +00:00
Aiden Cline
1da3550c4d ci: improve err msg 2025-12-29 17:46:29 -06:00
opencode
0c48e6a116 release: v1.0.209 2025-12-29 23:37:04 +00:00
Adam
ef266b2c74 fix(desktop): error page formatting 2025-12-29 17:33:59 -06:00
Aiden Cline
0a1cdc7a58 ci: use .env 2025-12-29 17:31:41 -06:00
Adam
2dec956a17 fix(desktop): better error messages 2025-12-29 17:29:56 -06:00
Aiden Cline
ef8388f0ee Revert "feat: read global ~/.claude/skills"
This reverts commit a1c9a1b8c5.
2025-12-29 17:20:04 -06:00
opencode-agent[bot]
e5c5b5e872 docs: add run from source guide (#6405)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-29 17:12:42 -06:00
Aiden Cline
a1c9a1b8c5 feat: read global ~/.claude/skills 2025-12-29 17:11:28 -06:00
Aiden Cline
76b012139a fix: add timeout to filewatcher subscriptions 2025-12-29 16:16:38 -06:00
Aiden Cline
02e5a19242 tweak: adjust git watcher to ignore files other than HEAD 2025-12-29 16:16:38 -06:00
Ivan Pantic
af967648cb docs: opencode notificator plugin (fixed link) (#6341)
Co-authored-by: Ivan Pantic <panta@talentkit.io>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-29 16:03:35 -06:00
Dominik Engelhardt
504a668a26 Set smallOptions for google models on openrouter (#6362) 2025-12-29 16:01:31 -06:00
ja
5efb1c7b2d feat(highlight): add nix syntax highlighting (#6386) 2025-12-29 15:53:41 -06:00
samcornor
fd973d242e fix(webfetch): make format parameter optional with markdown default (#6345)
Co-authored-by: Somair Ansar <somairansar@Somairs-MacBook-Air.local>
2025-12-29 15:53:12 -06:00
ja
c3d8672753 ci: use env vars for DRY workflow config (#6395) 2025-12-29 15:47:47 -06:00
Shoubhit Dash
fe8ef041f6 add supermemory plugin to ecosystem (#6399) 2025-12-29 15:46:22 -06:00
Frank
c841de947e zen: add gpt 5.1 codex mini 2025-12-29 16:44:11 -05:00
GitHub Action
825dfd48b1 chore: generate 2025-12-29 21:16:16 +00:00
Graham Bennett
923d114ffa Support different Nix store path prefixes (#6367) 2025-12-29 15:15:42 -06:00
Cole Leavitt
b157fd10a7 fix: filter messages with only step-start parts in toModelMessage (#6383) 2025-12-29 14:58:11 -06:00
ja
67ebe68160 feat(format): add nixfmt formatter for Nix files (#6380) 2025-12-29 14:57:52 -06:00
GitHub Action
7b63c14154 chore: generate 2025-12-29 20:50:47 +00:00
Adam
cdc11cde2e ignore: hide provider connect button until providers loaded 2025-12-29 14:50:06 -06:00
opencode
9721223b7e release: v1.0.208 2025-12-29 20:44:22 +00:00
Adam
35a626e711 fix(desktop): don't flash permissions with auto-accept 2025-12-29 14:40:53 -06:00
Adam
bb7b0ff221 fix(desktop): scroll sync 2025-12-29 14:36:27 -06:00
Adam
68b4038196 fix(desktop): more performance/scrolling fixes 2025-12-29 14:23:41 -06:00
Adam
3109214900 feat(desktop): auto-accept edits toggle 2025-12-29 14:23:41 -06:00
Adam
86ccc3409b fix(desktop): toast position 2025-12-29 14:23:41 -06:00
Github Action
a89089c88f Update Nix flake.lock and hashes 2025-12-29 20:06:20 +00:00
CasualDeveloper
e617c5d689 fix: prevent truncated Claude streams (#6388) 2025-12-29 14:04:53 -06:00
Frank
31983ca5ff zen: do not switch provider for models require stick provider 2025-12-29 14:27:51 -05:00
Aiden Cline
59e3b7409f chore: fix type error 2025-12-29 12:38:35 -06:00
Daniel Polito
b7ce46f7a1 Desktop: Image Preview and Dedupe File Upload (#6372) 2025-12-29 11:22:48 -06:00
Brett Heap
82b8d8fa5d fix(tui): make auth URLs clickable regardless of line wrapping (#6317)
Co-authored-by: brettheap <brett.heap@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2025-12-29 11:21:09 -06:00
Adam
77c837eb1a fix(desktop): throttle markdown renders 2025-12-29 11:19:40 -06:00
Adam
db77cc9845 chore: cleanup 2025-12-29 11:19:40 -06:00
Aiden Cline
68043edae6 ci: changelog script update (#6371) 2025-12-29 11:12:25 -06:00
Frainer Encarnación
337681dbbf fix(lsp): ESLint LSP server fails to auto-install on Windows (#6366) 2025-12-29 11:02:46 -06:00
Adam
66afc034d1 fix(desktop): don't show summary when already complete 2025-12-29 10:58:53 -06:00
Adam
11ab8de59f fix(desktop): markdown lists 2025-12-29 10:47:05 -06:00
Adam
5f074edc3a fix(desktop): performance/jankiness 2025-12-29 10:42:48 -06:00
Matt Silverlock
56b5cdf883 feat: install local plugin dependencies from package.json (#6302)
Co-authored-by: OpenCode <opencode@example.com>
2025-12-29 10:37:41 -06:00
Adam
fb0e1e4d8d Revert "fix(desktop): jankiness"
This reverts commit 831e9bce51.
2025-12-29 09:56:33 -06:00
Github Action
b745b1593f Update Nix flake.lock and hashes 2025-12-29 15:55:48 +00:00
Adam
7376c3f8e7 feat(desktop): latex support 2025-12-29 09:54:22 -06:00
Adam
831e9bce51 fix(desktop): jankiness 2025-12-29 09:47:57 -06:00
Adam
5de73abd82 fix(desktop): markdown styles 2025-12-29 09:47:57 -06:00
Zeno Jiricek
3adbbc1b23 docs: add opencode-skillful plugin to ecosystem page (#6333) 2025-12-29 09:32:44 -06:00
Frank
c6c29b3dcf zen: minimax m2.1 2025-12-29 10:16:32 -05:00
Adam
a687d7c15f fix(desktop): one permission at a time 2025-12-29 09:07:36 -06:00
Daniel Polito
0c6da69f39 Desktop: Edit Project (#6360) 2025-12-29 08:54:49 -06:00
Adam
c4930eb6b2 fix(desktop): more fine-grained state updates for permissions 2025-12-29 08:47:38 -06:00
Frank
a24549fce7 docs: update MiniMax console link in integration instructions 2025-12-29 09:29:01 -05:00
Adam
c0f9b13630 fix(desktop): more fine-grained state updates 2025-12-29 08:21:32 -06:00
Adam
98fd53fd5f fix(core): preserve imperative statements in summary 2025-12-29 07:25:55 -06:00
Adam
5b02a3029e fix(desktop): max height on edit tool calls 2025-12-29 07:03:44 -06:00
Frank
94e851c2a2 docs: add MiniMax integration instructions to providers documentation 2025-12-29 07:45:54 -05:00
GitHub Action
1658a3ff59 ignore: update download stats 2025-12-29 2025-12-29 12:05:06 +00:00
Adam
9c8bc64138 fix(desktop): sync last agent and model when changing session 2025-12-29 02:57:28 -06:00
Adam
80f704ebbf fix(desktop): context usage alignment 2025-12-29 02:47:51 -06:00
Matt Silverlock
4dae6d1fcf meta: use colors for agents (#5845) 2025-12-28 23:05:17 -06:00
Matt Silverlock
5d2cab39da docs: add compaction, watcher, experimental and provider options (#6304)
Co-authored-by: OpenCode <opencode@example.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-28 23:01:43 -06:00
GitHub Action
6963f96d4b chore: generate 2025-12-29 04:56:54 +00:00
Alice Alexandra Moore
05a9e7ce7a docs: clarify that MCP tools require glob patterns to disable (#6306)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-28 22:56:22 -06:00
GitHub Action
896d18ab3f chore: generate 2025-12-29 04:44:48 +00:00
Grégoire Morpain
893888536a fix(bedrock): support region and bearer token configuration (#6332)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-28 22:44:15 -06:00
GitHub Action
c6221fc8b3 chore: generate 2025-12-29 04:39:43 +00:00
Connor Adams
ae67f43ff0 feat: add support for .claude/skills directory (#6252) 2025-12-28 22:39:10 -06:00
opencode
76880dce0d release: v1.0.207 2025-12-29 01:57:53 +00:00
Adam
aafffb5b4b chore: cleanup 2025-12-28 19:54:22 -06:00
Adam
a71c9e3f2e fix(desktop): edit diffs 2025-12-28 19:49:39 -06:00
Adam
0156f03e0e chore: cleanup theme stuff 2025-12-28 19:27:36 -06:00
Frank
e0bb96a9f9 wip: bench 2025-12-28 20:00:49 -05:00
Daniel Polito
82e5d6d458 Desktop: Sync LSP updates (#6305) 2025-12-28 16:07:36 -06:00
Adam
a4411c21b6 feat(desktop): theme preview 2025-12-28 15:47:05 -06:00
Frank
9d61370ac4 sync 2025-12-28 15:33:18 -05:00
Frank
f3febd6e39 wip: benchmark 2025-12-28 14:55:05 -05:00
GitHub Action
f12d55bf1e chore: generate 2025-12-28 19:13:43 +00:00
Matt Silverlock
0c19b71f42 docs: add plugin configuration documentation (#6301)
Co-authored-by: OpenCode <opencode@example.com>
2025-12-28 13:13:11 -06:00
Mohak S
70fa66397e docs: add opencode-notifier plugin to ecosystem (#6283) 2025-12-28 13:09:38 -06:00
Daniel Polito
6e8cd3174c Include current working directory in local MCP transport (#6303) 2025-12-28 13:09:24 -06:00
GitHub Action
5bfffbe083 chore: generate 2025-12-28 19:06:59 +00:00
Didier Durand
29d8557d41 doc: fix typos in various files (#6294) 2025-12-28 13:06:25 -06:00
Didier Durand
ffd20b4477 chore: activate code coverage in bun test config (#6297) 2025-12-28 19:05:55 +00:00
opencode
2abaa46e23 release: v1.0.206 2025-12-28 19:05:54 +00:00
GitHub Action
0cbbb20d22 chore: generate 2025-12-28 18:54:55 +00:00
Frank
81c5e7b9ed wip: benchmark 2025-12-28 13:54:11 -05:00
opencode
ddf4897eaa release: v1.0.205 2025-12-28 18:37:58 +00:00
Adam
040939fb72 chore: cleanup theme stuff 2025-12-28 10:21:32 -06:00
Adam
f89b83a6d7 chore: cleanup theme stuff 2025-12-28 10:14:30 -06:00
Adam
82a876da4d chore: cleanup 2025-12-28 06:41:59 -06:00
GitHub Action
69a15ae9c1 ignore: update download stats 2025-12-28 2025-12-28 12:04:31 +00:00
Adam
18c8e5f451 chore: cleanup 2025-12-28 05:47:22 -06:00
Adam
ba3a1cfa0b chore: cleanup 2025-12-28 05:47:21 -06:00
Github Action
d8563160f7 Update Nix flake.lock and hashes 2025-12-28 11:13:54 +00:00
Adam
4a9ff9412e feat(desktop): themes 2025-12-28 05:12:36 -06:00
Matt Silverlock
d6db6ff198 fix: handle non-text response parts in GitHub action (#6173) 2025-12-27 21:24:10 -06:00
Aiden Cline
79c263494f tweak: inform agent if no skills are available 2025-12-27 21:20:00 -06:00
Adam
1b5bf32ce5 chore: permissions ux 2025-12-27 20:40:25 -06:00
Adam
2e972b3fdc fix(desktop): copy/paste in terminal 2025-12-27 20:18:59 -06:00
Adam
d70e9fb01e chore(desktop): cleanup 2025-12-27 19:59:16 -06:00
Adam
fc082a0f14 fix(desktop): drag file over entire body to attach 2025-12-27 19:49:35 -06:00
Adam
953e4e9446 chore(desktop): vertical tabs 2025-12-27 19:43:52 -06:00
rektide
7ea0d37ee3 Thinking & tool call visibility settings for /copy and /export (#6243)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-27 19:32:33 -06:00
scarf
e35d97f9d7 feat: add bash shell completions (#6239) 2025-12-27 19:14:56 -06:00
GitHub Action
2c0d9a46cb chore: generate 2025-12-28 01:12:02 +00:00
Nindaleth
2fe7a7f2d3 docs: document attach command (#6254)
Co-authored-by: Black_Fox <radekliska@gmail.com>
2025-12-27 19:11:30 -06:00
Connor Adams
8a2f4ddf70 chore: update INVALID_DIRS to include plural 'skills' directory (#6255) 2025-12-27 19:10:51 -06:00
processtrader
7a94d7a2c5 fix: stats command to correctly handle --days 0 for current day statistics (#6259)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-12-27 19:10:23 -06:00
Aiden Cline
de28fafb47 fix: search all recent models instead of only top 5 in TUI /models command 2025-12-27 19:07:38 -06:00
Ivan Pantic
9d485dd307 docs: add opencode-notificator to ecosystem plugins list (#6269)
Co-authored-by: Ivan Pantic <panta@talentkit.io>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-27 18:54:27 -06:00
GitHub Action
613813ac12 chore: generate 2025-12-28 00:53:48 +00:00
ewired
7617f59441 Allow line numbers and ranges in autocomplete (#4238) 2025-12-27 18:53:17 -06:00
opencode
7aecb43e84 release: v1.0.204 2025-12-27 20:51:09 +00:00
Adam
21eba5f987 feat(desktop): permissions 2025-12-27 14:43:42 -06:00
Adam
c523ca4127 wip(desktop): handle more errors 2025-12-27 14:33:22 -06:00
GitHub Action
685f3ea324 ignore: update download stats 2025-12-27 2025-12-27 12:04:27 +00:00
Aiden Cline
4667d57e3c ci: stale issues 2025-12-27 00:51:05 -06:00
Didier Durand
e6b9988fa4 doc: fix typos in various files (#6238) 2025-12-27 00:46:06 -06:00
rari404
3c02d5d338 feat: add path traversal protection to File.read and File.list (#5985) 2025-12-26 23:20:07 -06:00
Christopher Ochsenreither
bfb9787361 fix: compact command after revert now properly cleans up revert state (#6235) 2025-12-26 22:57:59 -06:00
ja
1bcc72c477 feat: add ability to disable spinner animation (#6084) 2025-12-26 22:12:35 -06:00
Adam
4385fa4dd7 fix(desktop): prompt input fixes, directory and branch in status bar 2025-12-26 20:47:13 -06:00
Dax Raad
2b054bec95 core: fix compaction config checks to properly respect user settings 2025-12-26 19:48:56 -05:00
Dax Raad
2cdc88d295 core: add compaction config tests to verify auto and prune settings work correctly 2025-12-26 19:44:32 -05:00
GitHub Action
f8fb08b3b4 chore: generate 2025-12-27 00:32:34 +00:00
Dax Raad
ed06de5e30 core: add configurable compaction settings to allow users to disable auto-compaction and pruning via config instead of flags 2025-12-26 19:31:48 -05:00
Frank
52b99622ad zen: add context for login errors 2025-12-26 17:32:39 -05:00
Github Action
a15397cd89 Update Nix flake.lock and hashes 2025-12-26 20:49:06 +00:00
GitHub Action
da394439a1 chore: generate 2025-12-26 20:48:30 +00:00
Adam
390b0a79b3 fix(core): mdns global config 2025-12-26 14:47:53 -06:00
Adam
b2f45d574f Reapply "feat(core): optional mdns service (#6192)"
This reverts commit 505068d5a6.
2025-12-26 14:47:53 -06:00
Aiden Cline
1e2ef07c97 chore: kill some unused tools 2025-12-26 14:31:22 -06:00
Aiden Cline
664e6bf2d0 test: add more tests to make sure that cwd is locked for read tool 2025-12-26 14:30:05 -06:00
Aiden Cline
160c8ab7cc tweak: bash tool description to avoid unnecessary 'cd &&' usage 2025-12-26 13:44:52 -06:00
Matt Silverlock
1626341a4a github: support issues and workflow_dispatch events (#6157) 2025-12-26 13:34:03 -06:00
Aiden Cline
61ddd1716d ci: re-enable sync zed 2025-12-26 12:24:14 -06:00
Aiden Cline
053a10e515 ci: fix token for gh 2025-12-26 12:22:56 -06:00
Aiden Cline
e1c1b1340b ci: fix var 2025-12-26 12:08:16 -06:00
Aiden Cline
7a5fbdf67c ci: update zed extension sync 2025-12-26 12:06:36 -06:00
Github Action
9afc451020 Update Nix flake.lock and hashes 2025-12-26 17:45:58 +00:00
GitHub Action
f4fdf0eb03 chore: generate 2025-12-26 17:45:03 +00:00
Aiden Cline
505068d5a6 Revert "feat(core): optional mdns service (#6192)"
This reverts commit 26e7043718.
2025-12-26 11:43:52 -06:00
Aiden Cline
2e10ffac6b chore: rm comments 2025-12-26 11:43:13 -06:00
Aiden Cline
4abaa052db fix: adjust upgrade command to use gh releases page if not npm/bun/pnpm install method 2025-12-26 11:43:12 -06:00
Rohan Godha
1bcf8d8806 fix: opencode web baseURL error (#6181) 2025-12-26 11:36:31 -06:00
Ariane Emory
25c68c8061 chore: kill the dead Polaris Alpha code (#6193) 2025-12-26 11:32:31 -06:00
ja
b0e4408ecf feat: add shfmt formatter for shell scripts (#6204) 2025-12-26 11:31:51 -06:00
Aiden Cline
8416db03ef tweak: make install script handle 404s better 2025-12-26 11:28:18 -06:00
Github Action
d5b47d9128 Update Nix flake.lock and hashes 2025-12-26 17:09:54 +00:00
GitHub Action
634559760a chore: generate 2025-12-26 17:09:31 +00:00
Ayush Walekar
155ba794cf chore: createOpencodeServer expose logLevel (#6202) 2025-12-26 11:09:06 -06:00
Roberto Carvajal
f1ab427f0e fix(dep): Update package.json - fix perplexity provider version (#6199)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-26 11:08:45 -06:00
Daniel Polito
2333af6ed3 Desktop: MCP UI (#6162)
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
2025-12-26 10:49:05 -06:00
GitHub Action
54588b4570 chore: generate 2025-12-26 16:30:20 +00:00
Adam
26e7043718 feat(core): optional mdns service (#6192)
Co-authored-by: Github Action <action@github.com>
2025-12-26 10:29:48 -06:00
GitHub Action
dd569c927a chore: generate 2025-12-26 16:22:05 +00:00
Didier Durand
cf38884778 doc: fix typos in various files (#6196) 2025-12-26 10:21:33 -06:00
GitHub Action
2946a6d9a7 ignore: update download stats 2025-12-26 2025-12-26 12:10:30 +00:00
Aiden Cline
3522c460e3 tweak: update transform for gemini models so that topP and topK match gemini-cli values 2025-12-25 22:46:12 -06:00
GitHub Action
b6a264819e chore: generate 2025-12-26 04:25:19 +00:00
JackNorris
46c7a41d5f fix: only show diagnostics block when errors exist (#6175) 2025-12-25 22:24:48 -06:00
opencode
7cc4b24ac2 release: v1.0.203 2025-12-26 04:10:11 +00:00
Dax Raad
281ce4c0c3 prompt update to prevent searching via bash tool 2025-12-25 23:07:39 -05:00
Donghyun Shin
f59d274d0f fix(lsp): make JDTLS use the correct config directory on Windows (#6121) 2025-12-25 21:17:54 -06:00
GitHub Action
8886c78dce chore: generate 2025-12-26 03:05:15 +00:00
Marco
d9f0f58277 feat: haskell lsp support (#6141) 2025-12-25 21:04:43 -06:00
opencode
effa7b45cf release: v1.0.202 2025-12-26 02:11:47 +00:00
Adam
b307075063 chore: brain icon 2025-12-25 20:06:41 -06:00
Adam
aaf9a5d434 fix(desktop): user message display 2025-12-25 19:45:20 -06:00
Adam
e9c2f1f3f3 fix(desktop): padding 2025-12-25 19:22:16 -06:00
Adam
7469cba7cf fix(desktop): move session context to top-right 2025-12-25 19:21:04 -06:00
Adam
5420702f69 fix(desktop): missing keybinds in tooltips 2025-12-25 19:07:42 -06:00
Adam
583751ecae fix(desktop): markdown rendering perf 2025-12-25 19:07:42 -06:00
GitHub Action
d0a1b5ef96 chore: generate 2025-12-26 01:03:22 +00:00
Adam
42f2bc7199 fix(desktop): can't collapse project with active session 2025-12-25 19:02:43 -06:00
Adam
603dae562a chore(ui): radio group primitive 2025-12-25 18:46:57 -06:00
Adam
650bd76370 feat(desktop): better indicator that session is busy 2025-12-25 14:31:10 -06:00
opencode
8aa3520683 release: v1.0.201 2025-12-25 14:07:19 +00:00
Adam
5b5b8c57d9 fix(desktop): so many prompt input fixes, merry christmas 2025-12-25 08:04:42 -06:00
GitHub Action
f057b22e20 ignore: update download stats 2025-12-25 2025-12-25 12:04:54 +00:00
GitHub Action
388d40e41f chore: generate 2025-12-25 05:59:27 +00:00
Dax Raad
f397c92ddf remove list tool 2025-12-25 00:58:47 -05:00
opencode
6f9bea4e1f release: v1.0.200 2025-12-25 04:39:32 +00:00
Adam
5c49b4cbfc fix(desktop): scroll jank in session turn and review 2025-12-24 22:36:45 -06:00
GitHub Action
b746e831e2 chore: generate 2025-12-25 04:17:39 +00:00
Adam
2178deef91 fix(desktop): override agent model 2025-12-24 22:16:58 -06:00
Adam
b1d2fb5319 fix(desktop): reconcile session diff updates 2025-12-24 22:16:44 -06:00
opencode
2284a4e6df release: v1.0.199 2025-12-25 02:40:19 +00:00
Adam
ad852d9186 chore: toast on file load error 2025-12-24 20:37:12 -06:00
Adam
8a9b4245b4 chore: cleanup dead code 2025-12-24 20:30:52 -06:00
Adam
76ac1ccb6b chore: show version on error page 2025-12-24 20:25:36 -06:00
Adam
e71bc8c0b0 fix(desktop): show server connection failure 2025-12-24 20:11:37 -06:00
Ahmed Mansour
a5301e2ab7 fix: correct Content-Type headers for static assets on app.opencode.ai (#6113) 2025-12-24 20:00:22 -06:00
Connor Adams
8eac72341f docs: update skills to use canonical ~/.config/opencode location (#6132) 2025-12-24 18:18:33 -06:00
Robb Tolliver
bd139b4bd6 docs: Corrected the number of built-in subagents in documentation (#6133) 2025-12-24 18:02:35 -06:00
GitHub Action
508578bf17 chore: generate 2025-12-24 19:21:10 +00:00
Dax Raad
607d8aafb7 tui: disable tips display in home route 2025-12-24 14:20:29 -05:00
Dax Raad
5843eca7d6 CI 2025-12-24 18:16:46 +00:00
opencode
ff3b68bd36 release: v1.0.198 2025-12-24 18:16:46 +00:00
Dax Raad
474b6fd3d1 ci 2025-12-24 13:12:29 -05:00
GitHub Action
6145b197f3 chore: generate 2025-12-24 18:08:42 +00:00
Dax Raad
918eff9233 ci 2025-12-24 13:07:56 -05:00
opencode
987e444828 release: v1.0.197 2025-12-24 17:47:07 +00:00
Dax Raad
99633cb299 Revert "feat: better styling for small screens (short and/or not wide) (#5968)"
This reverts commit ac371d2987.
2025-12-24 12:38:10 -05:00
GitHub Action
f822331eb8 chore: generate 2025-12-24 17:07:43 +00:00
Patrick Schiel
0f053769db docs: add infos about server debugging (#6085) 2025-12-24 11:07:12 -06:00
opencode
ceeaf494c4 release: v1.0.196 2025-12-24 16:40:16 +00:00
Adam
126d887e57 fix(desktop): last text part streaming 2025-12-24 10:35:52 -06:00
Adam
e5cfc24d6b fix(desktop): render perf 2025-12-24 10:26:49 -06:00
Jay V
7f8d659737 docs: edits 2025-12-24 11:23:51 -05:00
Jay V
4b061653f2 docs: add comprehensive CLI command documentation for agent, mcp, session, stats, and web commands 2025-12-24 11:12:09 -05:00
Jay V
eeed89f985 docs: make MCP server documentation more scannable and add Sentry example 2025-12-24 10:49:48 -05:00
Adam
8ab533b616 chore: cleanup 2025-12-24 09:07:31 -06:00
Adam
09a399d8d6 fix(desktop): summary flicker 2025-12-24 09:07:31 -06:00
Adam
b75575884a feat(desktop): show read tool args 2025-12-24 09:07:31 -06:00
GitHub Action
5688c9fd61 chore: generate 2025-12-24 14:56:15 +00:00
Adam
08a075df61 fix(desktop): better session navigation, hide child sessions 2025-12-24 08:55:32 -06:00
opencode
a2e8737114 release: v1.0.195 2025-12-24 14:50:40 +00:00
Adam
776a394b02 chore: cleanup 2025-12-24 08:46:11 -06:00
GitHub Action
5788b33fdf chore: generate 2025-12-24 14:38:25 +00:00
Adam
0f270c3da4 refactor(ui): rewrite createAutoScroll with robust event tracking to fix sticky behavior 2025-12-24 08:37:49 -06:00
opencode
376019e347 release: v1.0.194 2025-12-24 12:20:02 +00:00
Adam
44b773a6f6 chore: cleanup 2025-12-24 06:16:17 -06:00
Adam
df97774f7f fix(desktop): session sort when multiple active 2025-12-24 06:16:17 -06:00
Adam
eeff62a912 fix(share): page title should be session title 2025-12-24 06:16:17 -06:00
GitHub Action
3fc6c42f5f ignore: update download stats 2025-12-24 2025-12-24 12:04:46 +00:00
Adam
967d8238be fix(desktop): exclude deprecated models 2025-12-24 06:01:27 -06:00
Adam
bff7518a24 fix(desktop): auto-scroll 2025-12-24 05:57:48 -06:00
Adam
8eab677094 fix: don't disable text selection 2025-12-24 05:57:48 -06:00
Github Action
db57e7023a Update Nix flake.lock and hashes 2025-12-24 11:56:43 +00:00
Adam
ede4e467db deps: update marked and marked-shiki 2025-12-24 05:55:28 -06:00
Adam
aa1c560e5e fix(desktop): hang on backtracing-prone regex 2025-12-24 05:49:35 -06:00
Adam
3aca9e5fa5 fix(desktop): conditionally show review pane toggle 2025-12-24 05:22:25 -06:00
Ryan Vogel
9e96d83164 fix: remove SVG favicon to improve SEO (#5755) 2025-12-24 05:17:13 -06:00
Aiden Cline
4275907df6 docs: tweak lsp.mdx 2025-12-23 22:38:17 -06:00
opencode-agent[bot]
6097d6af86 docs: experimental LSP tool (#5943)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-23 22:37:49 -06:00
opencode-agent[bot]
09d2febe27 docs: skill tool/perm + parent keybind (#6001)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-23 22:25:55 -06:00
xiantang
2c5c1ecb5e docs: add Neovim to the list of editors (#6081) 2025-12-23 22:17:34 -06:00
Aiden Cline
99e2112807 tweak: retry err 2025-12-23 22:10:28 -06:00
GitHub Action
4b6575999d chore: generate 2025-12-24 01:37:35 +00:00
Frank
1a9ee3080c zen: sync 2025-12-23 20:36:55 -05:00
Abdelkader Boudih
f4d61be8bd feat(mcp): handle tools/list_changed notifications (#5913)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-23 19:36:37 -06:00
Aiden Cline
8b40e38cd7 test: add test for retry 2025-12-23 19:34:40 -06:00
Aiden Cline
7396d495ee chore: regen sdk 2025-12-23 19:34:38 -06:00
GitHub Action
f9b5ce180a chore: generate 2025-12-24 01:21:10 +00:00
Aiden Cline
12ee9d51c3 make 'The socket connection was closed unexpectedly' errors retryable 2025-12-23 19:20:31 -06:00
Rohan Mukherjee
2730e0c9cd chore: update AGENTS.md to ~150 lines (#5955) 2025-12-23 19:04:44 -06:00
David Hill
d6c81d6e14 style: update current todo style (#6077) 2025-12-23 18:57:02 -06:00
rari404
e8ac0b663b feat(tui): console copy-to-clipboard via opentui (#5658)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-23 18:46:01 -06:00
OpeOginni
2806f240ea fix: resize textarea when pasting prompt less than 150 chars (#6070) 2025-12-23 18:11:48 -06:00
Matt Silverlock
9898fbe8ef providers: add Cloudflare AI Gateway (#5174) 2025-12-23 17:31:58 -06:00
Frank
1bd8e61719 ci: adam is not a full stack engineer 2025-12-23 17:03:00 -05:00
Adam
b6c07cb1b8 fix: remove desktop dup 2025-12-23 15:31:18 -06:00
Viktor Nagy
83f23817ce Update gitlab.mdx to use the 2.x component version (#6062) 2025-12-23 14:56:46 -06:00
Github Action
23b1d7c755 Update Nix flake.lock and hashes 2025-12-23 20:42:17 +00:00
Aiden Cline
ef033db9c2 Revert "Add animated braille spinner to terminal title when agent is running (#5984)"
This reverts commit 59b87f60f7.
2025-12-23 14:40:55 -06:00
Aiden Cline
e30d5d8e34 tweak: update import & pr commands to use new share link ur 2025-12-23 14:04:33 -06:00
GitHub Action
698cfb57a1 chore: generate 2025-12-23 19:48:40 +00:00
Jon Redeker
27e72f2652 Add opencode-shell-strategy plugin to ecosystem (#5995) 2025-12-23 13:48:08 -06:00
ja
10eed6ee7e feat(install): add standard CLI flags (--help, --version, --no-modify-path) (#5885) 2025-12-23 13:47:32 -06:00
David Hill
59b87f60f7 Add animated braille spinner to terminal title when agent is running (#5984)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: Github Action <action@github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-23 13:38:10 -06:00
GitHub Action
d10089a0bf chore: generate 2025-12-23 19:29:05 +00:00
David Hill
ae7286c031 "Did you know?" start screen tips (#5982)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-23 13:28:33 -06:00
Daniel Gray
52048c327d fix: favorites and recents stay visible when filtering models (#6053) 2025-12-23 12:55:47 -06:00
Matt Silverlock
4e1a9b6216 docs: add MCP OAuth debugging section (#6047) 2025-12-23 12:46:59 -06:00
Aiden Cline
1995be3599 ci: update zed sync script 2025-12-23 12:45:34 -06:00
ja
86b9b7b15a fix(tui): prevent keybinds from executing when dialog is open (#6017) 2025-12-23 12:37:28 -06:00
Frank
a90f2b9723 ci: fix 2025-12-23 12:58:28 -05:00
GitHub Action
c73a17f8af chore: generate 2025-12-23 15:58:33 +00:00
Rhys Sullivan
48898fda07 [feat]: prompt stashing (#6021) 2025-12-23 09:58:00 -06:00
GitHub Action
c573732ddb chore: generate 2025-12-23 15:39:47 +00:00
Daniel Polito
ab2a6c45a3 Fix Github Pull Request Event (#6037) 2025-12-23 09:39:14 -06:00
opencode
66563fb974 release: v1.0.193 2025-12-23 15:16:20 +00:00
GitHub Action
fbece0dc4d chore: generate 2025-12-23 14:45:27 +00:00
Sebastian Herrlinger
1d9e181da0 indent wrapped todo items properly 2025-12-23 15:44:45 +01:00
opencode
c81721e9fc release: v1.0.192 2025-12-23 14:43:07 +00:00
Frank
a94899ed36 zen: glm 4.7 2025-12-23 07:56:37 -05:00
Frank
b18d22498c ci: fix 2025-12-23 07:53:33 -05:00
Frank
c75584a31b ci: fix 2025-12-23 07:38:42 -05:00
GitHub Action
b474f65547 ignore: update download stats 2025-12-23 2025-12-23 12:04:42 +00:00
Sebastian Herrlinger
c352999b41 no intermediate autocomplete result to avoid flickering 2025-12-23 12:22:34 +01:00
GitHub Action
f4cd708ca0 chore: generate 2025-12-23 10:15:49 +00:00
Brendan Allan
c20f2731ab desktop: kill_sidecar before update install on windows 2025-12-23 18:14:44 +08:00
Github Action
01ca1a384a Update Nix flake.lock and hashes 2025-12-23 10:12:37 +00:00
GitHub Action
f330dadd89 chore: generate 2025-12-23 10:11:53 +00:00
Adam
43e92b4932 deps: diffs, shiki updates 2025-12-23 04:08:42 -06:00
opencode
83397ebde2 release: v1.0.191 2025-12-23 05:57:23 +00:00
GitHub Action
fde74a72bb chore: generate 2025-12-23 05:53:02 +00:00
Brendan Allan
10ee8e5b3d console: add AppImage download link 2025-12-23 13:52:23 +08:00
GitHub Action
96d3f1fe7c chore: generate 2025-12-23 04:28:11 +00:00
Matt Silverlock
1a2b656c4d improve mcp CLI + ability to debug MCP oauth (#5980) 2025-12-22 22:27:38 -06:00
Aiden Cline
161e9287a8 ci: docs sync 2025-12-22 22:27:21 -06:00
opencode-agent[bot]
968543af39 docs: new /global/health API (#6006)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2025-12-22 22:26:47 -06:00
lif
5af35117db fix: handle Windows CRLF line endings in grep tool (#5948)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-22 22:26:15 -06:00
Joel Hooks
eab177f5e7 feat(plugin): allow compaction hook to replace prompt entirely (#5907) 2025-12-22 22:19:14 -06:00
Brendan Allan
279dc04b3c ci: rename tauri -> desktop 2025-12-23 11:15:19 +08:00
Github Action
cbc5903aa1 Update Nix flake.lock and hashes 2025-12-23 02:03:30 +00:00
Adam
81c3c63895 chore: rename packages/tauri -> packages/desktop 2025-12-22 20:01:25 -06:00
Github Action
b76bd4141d Update Nix flake.lock and hashes 2025-12-23 01:40:34 +00:00
Adam
794fe8f381 chore: rename packages/desktop -> packages/app 2025-12-22 19:39:00 -06:00
GitHub Action
a4eebf9f08 chore: generate 2025-12-23 01:17:33 +00:00
Adam
680a63e3de fix(desktop): better error messages on connection failure 2025-12-22 19:16:54 -06:00
Mohammad Alhashemi
3a54ab68d1 feat(skill): add per-agent filtering to skill tool description (#6000) 2025-12-22 20:14:33 -05:00
Frank
44fd0eee64 zen: glm 4.7 2025-12-22 19:36:07 -05:00
Aiden Cline
ac371d2987 feat: better styling for small screens (short and/or not wide) (#5968) 2025-12-22 18:00:26 -06:00
GitHub Action
a7baa5ce18 chore: generate 2025-12-22 23:40:52 +00:00
Dax Raad
b129f809b9 tui: change task tool container to block layout for better subagent session display 2025-12-22 18:40:15 -05:00
opencode
92c0ab51e2 release: v1.0.190 2025-12-22 23:31:20 +00:00
GitHub Action
b25418e68b chore: generate 2025-12-22 23:24:39 +00:00
Mohammad Alhashemi
046e351140 feat: add native skill tool with permission system (#5930)
Co-authored-by: Dax Raad <d@ironbay.co>
2025-12-22 18:24:06 -05:00
opencode
b9029afa22 release: v1.0.189 2025-12-22 23:15:23 +00:00
GitHub Action
b229aeec0b chore: generate 2025-12-22 22:47:22 +00:00
Jay V
c9140c6bab docs: edit gitlab 2025-12-22 22:47:22 +00:00
opencode
38551bda38 release: v1.0.188 2025-12-22 22:47:21 +00:00
Github Action
cd16d31510 Update Nix flake.lock and hashes 2025-12-22 22:37:48 +00:00
Frank
54ba1af5d6 remove sharp 2025-12-22 17:36:23 -05:00
Josh Thomas
fe3144ce5b fix(tui): resize textarea if text inserted via appendPrompt TUI API (#5983) 2025-12-22 16:29:18 -06:00
Frank
a1c0bae3af zen: add glm 4.7 2025-12-22 17:23:33 -05:00
Aiden Cline
85f8655dfd ignore: agents.md 2025-12-22 16:21:56 -06:00
Adam
9b6c9f64f7 feat(desktop): review pane toggle 2025-12-22 16:20:17 -06:00
Viktor Nagy
1aae1c795d Add gitlab-opencode to GitLab docs
The current GitLab page describes OpenCode integration through GitLab Duo.

GitLab Duo is a paying functionality and is limited to workflows supported by GitLab.

GitLab-OpenCode is a community project that offers more flexiblity, better customization and easier setup to use OpenCode in GitLab. On the downside, it does not have the level of integration into GitLab as Duo does.
2025-12-22 17:14:59 -05:00
Frank
526c723e62 support glm 4.7 2025-12-22 17:11:02 -05:00
GitHub Action
6011200128 chore: generate 2025-12-22 22:01:27 +00:00
Jay V
740fcd243c ignore: update GitHub stars to 41K and project stats to reflect current growth 2025-12-22 17:00:27 -05:00
opencode
e4d8a117c4 release: v1.0.187 2025-12-22 21:58:41 +00:00
Aiden Cline
8c4a816cf6 ci: add failure case for changelog 2025-12-22 15:53:41 -06:00
Aiden Cline
5605fc3f38 test: rm claude skills test 2025-12-22 15:45:31 -06:00
Aiden Cline
009b096004 fix: disable claude skill loading for now 2025-12-22 15:40:08 -06:00
Shpetim
64f898601b fix: stop auto execute on sendText vscode extension (#5994)
Co-authored-by: Shpetim <shpetim.alimi@ndbit.net>
2025-12-22 15:38:54 -06:00
Jon Redeker
224e5466c1 docs: add opencode-morph-fast-apply plugin to ecosystem (#5992) 2025-12-22 15:21:14 -06:00
Blake North
87b5b34280 fix(providers.opencode): check config for api key in addition to auth (#5906) 2025-12-22 15:20:40 -06:00
Github Action
855fd07d22 Update Nix flake.lock and hashes 2025-12-22 21:13:50 +00:00
Aiden Cline
f9be2bab3a fix: bundle more providers to fix breaking ai sdk issue 2025-12-22 15:12:18 -06:00
Rohan Godha
25f1643e8e feat(tui): go to parent keybind for subagents (#5762) 2025-12-22 14:50:45 -06:00
GitHub Action
e015bea462 chore: generate 2025-12-22 20:34:21 +00:00
wienans
7dc55ac3ca Add OpenChamber to ecosystem documentation (#5978) 2025-12-22 14:33:45 -06:00
ja
cd8ecf9722 feat(lsp): add Tinymist LSP support for Typst (#5933) 2025-12-22 14:31:47 -06:00
Github Action
eb021a5f92 Update Nix flake.lock and hashes 2025-12-22 20:27:29 +00:00
Sebastian Herrlinger
7f5e30834f upgrade opentui to v0.1.63, enabling kitty alternate keys by default 2025-12-22 21:26:03 +01:00
Tim Kleinschmidt
750a936ae1 support clojure projects with built-in lsp (#5975) 2025-12-22 14:20:15 -06:00
Shpetim
8dfef670b3 [FEATURE]: Show context usage in OpenCode Desktop Context usage (#5979) 2025-12-22 13:56:36 -06:00
Adam
1b1b73b5b3 fix(prompt): better summary prompt 2025-12-22 13:09:12 -06:00
Daniel Polito
6baee0791f docs: Github Auto Pull Request Docs (#5974) 2025-12-22 11:53:58 -06:00
Adam
291b65977c chore(desktop): auto scroll utility 2025-12-22 11:27:27 -06:00
GitHub Action
90f232d7f1 chore: generate 2025-12-22 17:06:35 +00:00
Will Marella
af214d35cb Add keybindable commands to navigate between user messages (#5078)
Co-authored-by: Will@Cambridge <willcambridge@MacBook-Pro-59.local>
Co-authored-by: Will@Cambridge <willcambridge@macbookpro.mynetworksettings.com>
2025-12-22 11:06:00 -06:00
Aiden Cline
3f0afd7cf6 ci: tweak docs prompt 2025-12-22 11:00:32 -06:00
Daniel Polito
0545c5da2d GitHub pull request event (#5335) 2025-12-22 10:59:02 -06:00
Adam
4a32fa6f02 fix(share): expanded state and responsiveness 2025-12-22 10:13:57 -06:00
Aiden Cline
29c99ed4ab ci: limit to opencode repo 2025-12-22 09:56:51 -06:00
Lekë Dobruna
753abbe164 fix: duplicate words in dialog options (#5944) 2025-12-22 09:56:32 -06:00
Adam
8e01f6cc13 fix(desktop): diff readability (colors) 2025-12-22 09:51:21 -06:00
Dax Raad
33c0b125cb fix url for web 2025-12-22 10:45:51 -05:00
GitHub Action
dab2e54df8 chore: generate 2025-12-22 14:38:20 +00:00
Buck Evan
60db171b44 fix(read): narrow .env file blocking to not block .envrc (#5654)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-22 08:37:43 -06:00
opencode
c6e9a5c800 release: v1.0.186 2025-12-22 12:14:06 +00:00
Adam
2c16b9fa61 Revert "server: ensure frontend has correct port for PTY websocket connections (#5898)"
This reverts commit a05915ddc8.
2025-12-22 06:05:46 -06:00
Adam
240ad31edd Revert "fix: server"
This reverts commit dbaac79039.
2025-12-22 06:05:46 -06:00
GitHub Action
a97631f769 ignore: update download stats 2025-12-22 2025-12-22 12:05:15 +00:00
Adam
dbaac79039 fix: server 2025-12-22 06:02:16 -06:00
Ashutosh Kumar
a05915ddc8 server: ensure frontend has correct port for PTY websocket connections (#5898) 2025-12-22 05:56:23 -06:00
Adam
eebbd73346 Revert "fix: use current page port instead of hardcoded 4096 (#5949)"
This reverts commit d04a72a4ad.
2025-12-22 05:55:15 -06:00
Adam
d4c981495a fix(desktop): cleanup auto scroll 2025-12-22 05:46:07 -06:00
Adam
653c206688 feat(desktop): mobile responsiveness 2025-12-22 05:46:07 -06:00
Adam
580f46b589 fix(desktop): filter child sessions from header 2025-12-22 05:46:07 -06:00
Adam
986d12fd20 feat(desktop): better task tool rendering 2025-12-22 05:46:06 -06:00
lif
d04a72a4ad fix: use current page port instead of hardcoded 4096 (#5949)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-22 05:45:08 -06:00
Aaron Iker
5fd873a35a feat: polish dialog & list styles for the desktop app, add fixed logos from models.dev (#5925) 2025-12-22 05:41:38 -06:00
Brendan Allan
a9fbd786b3 ci: fix tauri build args 2025-12-22 18:55:01 +08:00
Brendan Allan
abde984b3e ci: verbose build and re-enable appimage 2025-12-22 18:48:58 +08:00
GitHub Action
a95aa037a3 chore: generate 2025-12-22 10:06:29 +00:00
Brendan Allan
11a92b24c2 ci: run prepare step for tauri build 2025-12-22 18:05:52 +08:00
Brendan Allan
f9c10c62d8 ci: try downloading artifact in desktop prepare 2025-12-22 17:45:57 +08:00
NN708
6339f39871 feat(desktop): arm64 build for linux (#5935) 2025-12-22 03:45:30 -06:00
Brendan Allan
68b09b30a1 ci: replace with just upload-artifact whole dir 2025-12-22 17:17:17 +08:00
Brendan Allan
92ade2a320 ci: import bun shell 2025-12-22 17:10:43 +08:00
Brendan Allan
cb1a1fb26c try uploading artifacts in workflow 2025-12-22 17:08:44 +08:00
Brendan Allan
af5ebabd03 remove actions artifact uploading 2025-12-22 17:01:52 +08:00
Github Action
fe2626a4ea Update Nix flake.lock and hashes 2025-12-22 08:58:57 +00:00
GitHub Action
45447e3336 chore: generate 2025-12-22 08:58:16 +00:00
Brendan Allan
7a3e82ec5d ci: try to upload cli artifacts 2025-12-22 16:57:28 +08:00
Aiden Cline
345f4801e8 feat: add experimental lsp tool (#5886) 2025-12-22 00:34:21 -06:00
Luo Chen
ac4b8d62e3 feat: add nixd as lsp for nix language (#5929) 2025-12-22 00:30:08 -06:00
opencode-agent[bot]
236ce7a8c0 docs: Agent Skills (#5931)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-21 23:49:28 -06:00
Valerio Di Maggio
8bdc0c8f79 fix: ensure installation commands are using .quiet (#5758)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-21 23:44:25 -06:00
Ben Vargas
04650f01fe docs: add ai-sdk-provider-opencode-sdk to ecosystem (#5772) 2025-12-21 22:59:36 -06:00
Aiden Cline
02d4594abf ci: update docs prompt 2025-12-21 22:32:04 -06:00
Aiden Cline
c1894b4e3d ci: add automatic doc update workflow 2025-12-21 21:52:44 -06:00
Neil Daquioag
2062247e72 fix: support clipboard image paste (Ctrl+V) on Windows (#5919) 2025-12-21 21:18:47 -06:00
Aiden Cline
8785bec29c tweak: adjust minimax m2 topK and add minimax m2.1 topP 2025-12-21 21:17:58 -06:00
Noam Bressler
d4b7f75ce3 fix: Perform snapshot in cases finish-step is not reached (#5912)
Co-authored-by: noamzbr <noamzbr@users.noreply.github.com>
2025-12-21 21:13:11 -06:00
Matt Silverlock
4f73d58031 prompts: improve built-in /review prompt (#5918) 2025-12-21 21:11:48 -06:00
YeonGyu-Kim
b906f2de88 feat(server): expose auto param in session.summarize for plugins (#5924) 2025-12-21 21:05:30 -06:00
GitHub Action
4035afe5c8 chore: generate 2025-12-22 00:45:30 +00:00
Dax
8fe0715928 feat: add Agent Skills support (#5921) 2025-12-21 19:44:56 -05:00
opencode
cb8af962cd release: v1.0.185 2025-12-21 23:38:40 +00:00
Dax Raad
c333ffa38b core: fix LSP server binary installation and shell command execution
- Ensure proper file permissions are set for installed LSP binaries on non-Windows platforms
- Add error handling for shell command execution in prompt system to prevent crashes
2025-12-21 18:33:37 -05:00
Aiden Cline
3456f4ed80 tweak: update kimi-k2 and kimi-k2-thinking to use recommended temperature values 2025-12-21 15:54:57 -06:00
Dax Raad
2536e9f45b tui: fix SDK context usage and server port fallback
- Update SDK context to return client instead of event for proper usage
- Add server port fallback to 4096 when port 0 is specified but unavailable
- Fix SDK event listener usage in TUI app
2025-12-21 14:57:55 -05:00
Github Action
9188bc542c Update Nix flake.lock and hashes 2025-12-21 17:46:30 +00:00
GitHub Action
cbaba10994 chore: generate 2025-12-21 17:45:51 +00:00
Sherlock Holmes
85d3604309 fix(deps): add missing @opencode-ai/plugin to dependencies (#5797) 2025-12-21 11:45:20 -06:00
Nalin Singh
507ba644cf feat: add syntax highlighting for .ets files (#5889) 2025-12-21 11:42:47 -06:00
Adam Hosker
3d6f62746a fix: prevent stats workflow from running on forks (#5897) 2025-12-21 11:32:00 -06:00
Abdelkader Boudih
2f48c8c05f fix: use official MCP SDK for better tool schema handling (#5463) 2025-12-21 11:31:07 -06:00
GitHub Action
4828fd1eac chore: generate 2025-12-21 14:47:27 +00:00
Matt Silverlock
10375263ef github: support schedule events (#5810) 2025-12-21 08:46:54 -06:00
GitHub Action
ae00001aa0 ignore: update download stats 2025-12-21 2025-12-21 12:04:17 +00:00
opencode
f53ebafbab release: v1.0.184 2025-12-21 11:04:32 +00:00
Adam
23ebc50da9 fix(desktop): layout regression 2025-12-21 05:01:30 -06:00
Adam
673c6f97b7 fix(desktop): better keybind tooltips 2025-12-21 04:56:20 -06:00
Adam
ec46f71258 fix(desktop): todo tool title 2025-12-21 04:41:54 -06:00
Adam
8865e524cb fix(desktop): allow text selection 2025-12-21 04:39:54 -06:00
GitHub Action
36bb02ae45 chore: generate 2025-12-21 10:36:43 +00:00
Adam
5072331f04 fix(desktop): incorrect state dir on macos 2025-12-21 04:36:02 -06:00
opencode
9d48fd4bbd release: v1.0.183 2025-12-21 10:14:41 +00:00
Adam
bf66390557 fix(desktop): better error reporting 2025-12-21 04:11:09 -06:00
Adam
184643f0db fix(desktop): non-latin file paths failed 2025-12-21 04:06:10 -06:00
Adam
1bce898ca7 fix(desktop): file loading errors 2025-12-21 04:02:00 -06:00
Github Action
8c895570c6 Update Nix flake.lock and hashes 2025-12-21 06:11:08 +00:00
Christopher Tso
6dc4e5ac93 Make CLI build script Windows-friendly (#5835) 2025-12-21 00:09:58 -06:00
Aiden Cline
d3922f0965 core: add verification that at least 1 primary agent is enabled, add regression tests (#5881) 2025-12-20 21:36:22 -06:00
GitHub Action
cfaac9f2e1 chore: generate 2025-12-21 03:07:26 +00:00
opencode
0b046d6cf0 release: v1.0.182 2025-12-21 03:07:26 +00:00
Aiden Cline
3d822e5f79 fix: regression where config would error despite valid agents 2025-12-20 21:04:37 -06:00
opencode
f9cef22a53 release: v1.0.181 2025-12-21 02:06:25 +00:00
Adam
b5d7d3dec1 fix(desktop): layout 2025-12-20 20:02:40 -06:00
Adam
182630e0d7 feat(desktop): new layout 2025-12-20 19:52:12 -06:00
YuY801103
c81506b28d docs: add Traditional Chinese (Taiwan) README translation (#5861)
Co-authored-by: Yu <YuY801103@users.noreply.github.com>
2025-12-20 15:51:42 -06:00
Ryan Vogel
6c40bfe043 docs: clarify model ID format for OpenCode provider (#5854) 2025-12-20 13:51:13 -06:00
Aiden Cline
9caaae6a18 tweak: better error message if no primary agents are enabled 2025-12-20 13:47:28 -06:00
Ryan Vogel
ad6a5e6157 feat(docs): adding .md to docs pages shows raw markdown (#5823) 2025-12-20 12:05:06 -06:00
shamil2
7dd8ea58c2 feat: add Catppuccin Frappé theme (#5821)
Co-authored-by: shamil2 <shamil2@users.noreply.github.com>
2025-12-20 12:04:35 -06:00
ja
3b261e0125 docs: add name property to model configuration example (#5853) 2025-12-20 11:54:49 -06:00
Shpetim
426791f68a fix: system theme flicker (#5842)
Co-authored-by: Shpetim <shpetim.alimi@ndbit.net>
2025-12-20 11:53:46 -06:00
Frank
c7cade2494 zen: sync 2025-12-20 12:52:55 -05:00
Matt Silverlock
8f6c8844d7 feat: support configuring a default_agent across all API/user surfaces (#5843)
Co-authored-by: observerw <observerw@users.noreply.github.com>
2025-12-20 11:49:23 -06:00
Aiden Cline
da6e0e60c0 ci: adjust review agent prompt to discourage bad diffs 2025-12-20 11:43:59 -06:00
lif
d89b567b47 fix: add transform case for gemini if mcp tool has missing array items (#5846) 2025-12-20 11:41:52 -06:00
ja
34eb03f5b8 fix: prioritize session list loading when resuming with -c (#5816)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-20 11:39:26 -06:00
Ryan Cassidy
2f6d15a51e feat: add cursor theme (#5850) 2025-12-20 10:56:27 -06:00
GitHub Action
8ffea80980 chore: generate 2025-12-20 16:00:36 +00:00
opencode
c87d61b561 release: v1.0.180 2025-12-20 16:00:36 +00:00
Dax Raad
35c12e2053 ci 2025-12-20 10:54:02 -05:00
opencode
33d8bfc937 release: v1.0.179 2025-12-20 15:38:38 +00:00
Dax Raad
f2343a6794 ci 2025-12-20 10:35:01 -05:00
GitHub Action
bab000eeb5 chore: generate 2025-12-20 15:29:23 +00:00
Dax Raad
8e674ae053 ci 2025-12-20 10:28:46 -05:00
opencode
6a4f4009d5 release: v1.0.178 2025-12-20 15:08:27 +00:00
Dax Raad
5e79b95927 ci 2025-12-20 10:05:03 -05:00
Tommy D. Rossi
a7a2bbb497 feat: add endpoints to delete and update message parts (#5433) 2025-12-20 15:00:41 +00:00
GitHub Action
6e93d14bdb chore: generate 2025-12-20 15:00:41 +00:00
opencode
f29f284b3e release: v1.0.177 2025-12-20 15:00:40 +00:00
Dax Raad
b1b8f6cf71 ci 2025-12-20 09:57:19 -05:00
opencode
4c3336bbe7 release: v1.0.176 2025-12-20 12:28:27 +00:00
Adam
354ac0b493 fix(desktop): sidebar UX issues 2025-12-20 06:25:39 -06:00
Adam
1d159c6858 fix(desktop): task rendering perf 2025-12-20 06:15:59 -06:00
GitHub Action
d70639b256 chore: generate 2025-12-20 12:13:52 +00:00
Adam
e4a92f0084 fix(desktop): show last text part when summarized 2025-12-20 06:12:58 -06:00
Adam
fdf5a70a27 fix(desktop): performance with lots of session changes 2025-12-20 06:12:58 -06:00
Adam
f71da42520 fix(desktop): event reconnect gaps 2025-12-20 06:12:58 -06:00
GitHub Action
f6bdeb9e3a ignore: update download stats 2025-12-20 2025-12-20 12:04:14 +00:00
opencode
2400354bab release: v1.0.175 2025-12-20 11:10:23 +00:00
Adam
db348c46cc fix(desktop): perf tweaks 2025-12-20 05:06:57 -06:00
Adam
49567fe61a fix(desktop): add retries to init promises 2025-12-20 04:57:39 -06:00
Adam
e5b3f796e4 fix: types 2025-12-20 04:33:42 -06:00
Adam
a9700c8773 fix: shouldEncode 2025-12-20 04:25:17 -06:00
Adam
26cf5e003e fix(desktop): perf stuff 2025-12-20 04:25:17 -06:00
Adam
742cf10dee fix(desktop): removed projects 2025-12-20 04:25:16 -06:00
Frank
7664453f94 zen: add minimax m2.1 2025-12-20 01:55:10 -05:00
GitHub Action
460672aa93 chore: generate 2025-12-20 06:27:02 +00:00
Frank
b4e4fd9807 zen: add minimax m2.1 2025-12-20 01:26:27 -05:00
opencode
34bdfd0937 release: v1.0.174 2025-12-20 04:06:28 +00:00
GitHub Action
84591ca8ad chore: generate 2025-12-20 03:58:36 +00:00
Aiden Cline
fd4d0c5c0b fix: file permissions 2025-12-19 21:57:55 -06:00
opencode
9f5db46911 release: v1.0.173 2025-12-20 03:48:15 +00:00
Ariane Emory
755ddbb223 feat(tui): reinsert forked message text in prompt text input box when forking session (resolves #5495) (#5545)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-19 21:45:33 -06:00
GitHub Action
701d470d01 chore: generate 2025-12-20 03:42:00 +00:00
Aiden Cline
1d9058d26b ci: fix archive 2025-12-19 21:41:18 -06:00
opencode
39e2a5f595 release: v1.0.172 2025-12-20 03:29:07 +00:00
Aiden Cline
f862ab6722 ci: fix undefined 2025-12-19 21:22:49 -06:00
Aiden Cline
129d4f0b1b ci: fix release notes 2025-12-19 17:49:07 -06:00
Aiden Cline
3a1e50d1f8 Revert "tweak: better release notes (grouped changelog) (#5768)"
This reverts commit b99afdad91.
2025-12-19 17:46:38 -06:00
Aiden Cline
e2fb690d8e Revert "tweak: DevEx to run changelog independently (#5774)"
This reverts commit 7f8e799392.
2025-12-19 17:46:36 -06:00
Aiden Cline
0a7f58a811 Revert "ci: gemini 3 flash doesnt exist in pinned cicd version (#5776)"
This reverts commit 382905602c.
2025-12-19 17:46:35 -06:00
Aiden Cline
dae0168ed8 test: fixture cleanup 2025-12-19 17:00:23 -06:00
Aiden Cline
edfe2e4f1c test: fix test case 2025-12-19 16:37:36 -06:00
Kaspar
1bc1ea8b47 tweak: Make LSP message more accurate when LSPs disabled (#5814) 2025-12-19 16:34:04 -06:00
Aiden Cline
dacbbe3184 chore: rm dead code 2025-12-19 16:20:17 -06:00
Aiden Cline
89285d8f5f ci: fix publish auth failure 2025-12-19 16:00:41 -06:00
1XD
2e853911c3 docs: replace deprecated mise ubi backend with github backend (#5811) 2025-12-19 15:51:09 -06:00
Github Action
695fdecf23 Update Nix flake.lock and hashes 2025-12-19 20:40:07 +00:00
Frank
054d22791d zen: sync 2025-12-19 15:38:31 -05:00
Frank
4a57cc69d8 zen: sync 2025-12-19 15:25:35 -05:00
Frank
7e0c8db029 zen: sync 2025-12-19 15:21:57 -05:00
Cameron
ba4cc3bf86 Desktop file encoding issue (#5490) 2025-12-19 14:18:37 -06:00
Adam
b19a424c85 chore: cleanup 2025-12-19 13:11:08 -06:00
Adam
1689281c35 fix(desktop): auto-scroll and session perf 2025-12-19 13:07:53 -06:00
Adam
cdbb59fae8 fix(desktop): don't use tauri http for sse events 2025-12-19 13:07:53 -06:00
Adam
4eb311e98f fix(desktop): error height 2025-12-19 13:07:52 -06:00
Github Action
80eac96258 Update Nix flake.lock and hashes 2025-12-19 18:29:37 +00:00
Aiden Cline
4bad6f9f1b tweak: use fetch instead of octokit for now 2025-12-19 12:28:11 -06:00
Steven T. Cramer
d7db57e8e1 docs: add Windows Terminal Shift+Enter configuration guide (#5788) 2025-12-19 12:27:21 -06:00
Brendan Allan
943fbf39a3 ci: separate standalone publishing from dependent publishing (#5634)
Co-authored-by: GitHub Action <action@github.com>
2025-12-19 13:22:17 -05:00
Dax Raad
d8a34c2fcc core: prevent file system scanning when in root directory to avoid unnecessary operations 2025-12-19 13:15:11 -05:00
Aiden Cline
5720ed1f44 ci: change token for gh release to allow discord job to actually trigger see: https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow 2025-12-19 10:45:26 -06:00
GitHub Action
bb20a359e4 chore: generate 2025-12-19 14:56:47 +00:00
José Valim
0d472a49a0 Do not include hidden agents in ACP (#5791) 2025-12-19 08:56:12 -06:00
Github Action
203581e82f Update Nix flake.lock and hashes 2025-12-19 13:42:49 +00:00
Sebastian Herrlinger
677631916c upgrade opentui to v0.1.62, enabling textarea mouse scroll and cursor set 2025-12-19 13:42:49 +00:00
Github Action
1aa1e8c904 Update Nix flake.lock and hashes 2025-12-19 13:42:49 +00:00
opencode
55d62fbd9f release: v1.0.170 2025-12-19 13:42:48 +00:00
Adam
e1ad2a355c fix(desktop): error handling 2025-12-19 07:38:38 -06:00
Adam
4f318f913e chore: logging 2025-12-19 07:38:38 -06:00
Adam
2d814b6db2 fix(desktop): separate prompt history for shell 2025-12-19 07:38:38 -06:00
Adam
e561f1ad68 fix(desktop): don't navigate prompt history if dirty 2025-12-19 07:38:37 -06:00
Sebastian Herrlinger
ebfb985215 user messages as markdown with toggle 2025-12-19 13:51:26 +01:00
GitHub Action
2646da50df ignore: update download stats 2025-12-19 2025-12-19 12:04:33 +00:00
Github Action
50a5f6e53b Update Nix flake.lock and hashes 2025-12-19 11:49:40 +00:00
Brendan Allan
d03fac52e7 Update SolidStart and bring back HttpHeader usage (#5355)
Co-authored-by: Github Action <action@github.com>
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
2025-12-19 05:46:33 -06:00
Sherlock Holmes
6a802c01cd feat(tui): implement smooth scrolling for autocomplete dropdown navigation (#5559)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-19 00:40:16 -06:00
Eric Shirley
14146428dd lsp: add oxlint server (#5570) 2025-12-19 00:17:20 -06:00
Aiden Cline
26d0280f70 docs: contributing 2025-12-18 22:18:34 -06:00
Aiden Cline
3274a5813e ci: only run generate for dev 2025-12-18 22:17:36 -06:00
Luke Parker
382905602c ci: gemini 3 flash doesnt exist in pinned cicd version (#5776) 2025-12-18 21:59:46 -06:00
GitHub Action
8b5cea7899 chore: generate 2025-12-19 03:59:14 +00:00
Matt Silverlock
100c31cbb1 fix: use correct octokit API for PR review comment reactions (#5778) 2025-12-18 21:58:41 -06:00
GitHub Action
0b286f1b84 chore: generate 2025-12-19 02:12:35 +00:00
Brendan Allan
2f6ca958fe tauri: remove pinch-to-zoom on window 2025-12-19 02:12:35 +00:00
Basit Mustafa
5218e7a546 docs(ecosystem): add opencode-zellij-namer plugin (#5771) 2025-12-19 02:12:35 +00:00
Luke Parker
7f8e799392 tweak: DevEx to run changelog independently (#5774) 2025-12-19 02:12:35 +00:00
opencode
289f4abaaa release: v1.0.169 2025-12-19 02:12:34 +00:00
Adam
7ce898ce43 fix(desktop): shell mode 2025-12-18 20:06:53 -06:00
Adam
0dd716a75e fix(desktop): extra reqs 2025-12-18 19:53:38 -06:00
Aiden Cline
87171467fa ci: better err msg for generate workflow 2025-12-18 19:03:16 -06:00
Luke Parker
b99afdad91 tweak: better release notes (grouped changelog) (#5768) 2025-12-18 18:49:37 -06:00
Aiden Cline
4fd576f3af fix: better api call error msgs in some cases 2025-12-18 18:46:25 -06:00
GitHub Action
2f41d0bedd chore: generate 2025-12-19 00:18:07 +00:00
Rohan Godha
5f03290534 feat(tui): click on subagents to open them (#5761)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-18 18:17:34 -06:00
opencode
427157c683 release: v1.0.168 2025-12-18 21:55:29 +00:00
Github Action
a0ab3d98b7 Update Nix flake.lock and hashes 2025-12-18 21:48:53 +00:00
GitHub Action
c8de766913 chore: generate 2025-12-18 21:47:59 +00:00
Adam
d57b963141 fix: id 2025-12-18 15:47:21 -06:00
Adam
0ebcaff927 fix(desktop): expanded states 2025-12-18 15:47:20 -06:00
Adam
15931fa170 chore: cleanup 2025-12-18 15:47:20 -06:00
Adam
af4087d7b5 fix(desktop): smaller max-width when review open 2025-12-18 15:47:20 -06:00
Aiden Cline
323ea1040c ci: fix generate 2025-12-18 15:23:23 -06:00
Aiden Cline
1fe87b0233 ci: fix file perm 2025-12-18 14:39:44 -06:00
Aiden Cline
8d11df1b3b ci: handle case where generate.yml fails better 2025-12-18 14:33:40 -06:00
Aiden Cline
ecc5050838 tweak: more retry cases 2025-12-18 13:59:37 -06:00
Aiden Cline
606cf3b6f2 chore: rm dead code 2025-12-18 13:59:37 -06:00
GitHub Action
67cfd7f06b chore: format code 2025-12-18 19:38:25 +00:00
OpeOginni
ab9ac7c87a feat: add experimental support for Ty language server (#5575) 2025-12-18 13:37:48 -06:00
Adam
ee9f979613 fix(desktop): markdown styles 2025-12-18 13:03:14 -06:00
Adam
228b6444f8 fix(desktop): don't show image button in shell mode 2025-12-18 13:03:14 -06:00
Frank
9998efdae2 zen: cleanup headers 2025-12-18 13:47:31 -05:00
Aiden Cline
9427f56e1a rm interleaved thinking filter for certain kimi k2 thinking model providers that were bugged 2025-12-18 12:26:27 -06:00
Adam
a6dd35d73d fix(desktop): submit prompt 2025-12-18 12:03:21 -06:00
GitHub Action
faeaafa5f5 chore: format code 2025-12-18 17:31:49 +00:00
Matt Silverlock
8b298a233e github: add OIDC_BASE_URL for custom GitHub App installs (#5756) 2025-12-18 11:31:13 -06:00
Adam
6f43d03043 fix(desktop): checkbox render in safari fml 2025-12-18 11:16:33 -06:00
Adam
c868a4088d fix(desktop): rendering shell mode messages 2025-12-18 11:16:33 -06:00
Adam
83d8a88c90 fix(desktop): error styles 2025-12-18 11:16:33 -06:00
Adam
268f37f8c9 fix(desktop): prompt history nav, optimistic prompt dup 2025-12-18 11:16:33 -06:00
Adam
b0aaf04957 fix(desktop): session ordered by most recent 2025-12-18 11:16:32 -06:00
Adam
b7875256f3 feat(desktop): shell mode 2025-12-18 11:16:32 -06:00
Adam
7bc47fb904 chore: cleanup 2025-12-18 11:16:32 -06:00
GitHub Action
5cf8e54372 chore: format code 2025-12-18 16:39:21 +00:00
Ariane Emory
7437ccd6f4 feat(tui): fork slash command for keyboard-friendly session forking (resolves #5599) (#5610)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-18 10:38:19 -06:00
Jeon Suyeol
4bf882ba81 fix(command): validate model before executing slash command (#5740) 2025-12-18 10:35:40 -06:00
Frank
d5dcc55a47 Revert "add client header"
This reverts commit 2fb89161c8.
2025-12-18 11:21:22 -05:00
barış
e1925f4fe8 docs: fix typos (#5753) 2025-12-18 09:56:37 -06:00
Aiden Cline
ee3d034e16 ci: fix discord 2025-12-18 09:56:08 -06:00
Aiden Cline
257a4d5b86 bump bun version 2025-12-18 09:47:42 -06:00
Daniel Polito
1fc5836f64 Improve Github Action Hallucinations (#5751) 2025-12-18 09:40:04 -06:00
Frank
2fb89161c8 add client header 2025-12-18 10:33:25 -05:00
GitHub Action
251fbc0a99 ignore: update download stats 2025-12-18 2025-12-18 12:16:54 +00:00
Brendan Allan
0da901a188 tauri: disable pinch zoom on linux (#5735) 2025-12-18 05:13:09 -06:00
Brendan Allan
17221e6ffe tauri: configure display backends more correctly on linux (#5730) 2025-12-18 18:34:12 +08:00
GitHub Action
cc9f88ac8f chore: format code 2025-12-18 10:28:55 +00:00
Adam
fe65ed6a61 fix(desktop): disable pinch to zoom 2025-12-18 04:28:03 -06:00
Adam
e37a75a411 feat(desktop): custom update toast 2025-12-18 04:26:21 -06:00
opencode
194ff4919c release: v1.0.167 2025-12-18 09:45:33 +00:00
shuv
83843a794f fix: handle empty directory query parameter in server middleware (#5732) 2025-12-18 03:27:50 -06:00
Brendan Allan
235a60d3c2 tauri: say OpenCode Server instead of OpenCode CLI 2025-12-18 17:18:46 +08:00
Brendan Allan
b70d186bd1 tauri: server spawn fail dialog w/ copy logs button (#5729) 2025-12-18 03:17:31 -06:00
Frank
647331de28 zen: error handling for stream requests 2025-12-18 00:47:37 -05:00
GitHub Action
57ef115375 chore: format code 2025-12-18 04:30:54 +00:00
Jeon Suyeol
942498211f docs: add OPENCODE_DISABLE_TERMINAL_TITLE to environment variables (#5725) 2025-12-17 22:30:21 -06:00
Jake Nelson
e789fcf5e5 feat(tui): add option to disable terminal title (#5713) 2025-12-17 22:30:01 -06:00
Frank
b9fb180bc6 zen: error handling for stream requests 2025-12-17 22:55:44 -05:00
Rohan Mukherjee
7427b887f9 MCP improvements (#5699) 2025-12-17 20:49:45 -06:00
Jay V
289b2b6a51 docs: add legal pages with privacy policy and terms of service links 2025-12-17 20:17:33 -05:00
GitHub Action
49b4b5907e chore: format code 2025-12-18 01:09:02 +00:00
Ryan Vogel
f82442c123 docs: add opencode.cafe to ecosystem page (#5714) 2025-12-17 19:08:29 -06:00
opencode
e682cc9daf release: v1.0.166 2025-12-17 22:08:15 +00:00
Adam
d359e086a4 chore: cleanup 2025-12-17 16:04:41 -06:00
Adam
f949755367 fix: better init error messages 2025-12-17 16:04:40 -06:00
Adam
a168d854f4 fix: auto-scroll 2025-12-17 16:04:40 -06:00
GitHub Action
31645f5578 chore: format code 2025-12-17 22:03:43 +00:00
Sercan Sagman
a1b68daa9a fix(tui): exclude reverted assistant reply when copying last message (#5705)
Signed-off-by: assagman <ahmetsercansagman@gmail.com>
2025-12-17 16:03:06 -06:00
opencode
ca65da2d9e release: v1.0.165 2025-12-17 21:46:21 +00:00
Adam
e48d804d84 feat(desktop): startup errors shown 2025-12-17 15:42:55 -06:00
Adam
b4209582fb feat(desktop): optimistic prompt submit 2025-12-17 15:42:55 -06:00
Aiden Cline
dbdea2f659 fix: better error messages 2025-12-17 15:32:44 -06:00
Aiden Cline
a50ab4b5b5 fix: prevent 1 from showing when preparing write 2025-12-17 15:25:04 -06:00
Nalin Singh
4d7c3f56fa feat: add viewportOptions to scrollbox for padding adjustments to avoid scrollbar overlap (#5703) 2025-12-17 15:09:41 -06:00
Spoon
16b41d2bea UI: show plugins in /status (#4515)
Co-authored-by: GitHub Action <action@github.com>
2025-12-17 15:01:52 -06:00
Nalin Singh
a8c499ae8f fix: prevent session list selection from jumping to active session when confirming delete (#5666) 2025-12-17 14:35:46 -06:00
Joel Hooks
24430287c5 feat(plugin): add experimental.session.compacting hook for pre-compaction context injection (#5698)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-17 13:57:09 -06:00
Github Action
1f52731255 Update Nix flake.lock and hashes 2025-12-17 19:13:06 +00:00
Adam
4a3ba58f65 chore: localStorage -> tauri store 2025-12-17 13:11:02 -06:00
Brendan Allan
2a3a8a1ec2 console: use download proxy to rename mac and windows installers (#5697)
Co-authored-by: GitHub Action <action@github.com>
2025-12-18 01:59:23 +08:00
Ravi Kumar
69e562125d fix(tui): resolve session_status TypeError (#5520) 2025-12-17 11:38:05 -06:00
Aiden Cline
b5e97eb338 fix: keep session dialog open if deleting session 2025-12-17 11:29:55 -06:00
Aiden Cline
16e6941495 fix: remove needless tui event publish on session delete 2025-12-17 11:29:55 -06:00
GitHub Action
f033e0317e chore: format code 2025-12-17 17:02:36 +00:00
Adam
ddd88f92cc fix: sticky visual issues 2025-12-17 11:02:00 -06:00
Aiden Cline
99101edc13 ci: add windows label to triage bot 2025-12-17 11:01:53 -06:00
GitHub Action
6e85a07977 chore: format code 2025-12-17 16:59:57 +00:00
Brendan Allan
be1a3536ae console: add /download/[platform] endpoint 2025-12-18 00:59:16 +08:00
Qio
1e4bfbcf6f add OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX to override 32k default (#5679)
Co-authored-by: qio <handsomehust@gmail.com>
2025-12-17 10:35:43 -06:00
Adam
204e3bf382 feat(desktop): inter and ibm plex mono 2025-12-17 10:30:35 -06:00
Adam
8fb014a48d feat(desktop): inter and ibm plex mono 2025-12-17 10:30:03 -06:00
Paolo Ricciuti
57c3cf1f8b fix: send mcpName as state if authUrl doesn't have state (#5681) 2025-12-17 10:26:23 -06:00
Aiden Cline
f9d0850c5e test: add regression test for setCacheKey option 2025-12-17 10:24:53 -06:00
Spoon
8864da7a77 batch: enable edit, todoread, clarify error message, minor tool description change (#5659) 2025-12-17 10:23:35 -06:00
Rhys Sullivan
1b39199083 fix: change subagent navigation order to newest-to-oldest (#5680) 2025-12-17 10:22:57 -06:00
Shantur Rathore
b8204c0bb7 fix: config option setCacheKey not being respected (#5686) 2025-12-17 10:20:10 -06:00
Aiden Cline
fe8c5c143e docs: update share link 2025-12-17 10:18:30 -06:00
Frank
d6f86e9bb7 zen: add gemini 3 flash 2025-12-17 11:10:58 -05:00
Brendan Allan
bf00b2bfc9 tauri: nsis header and sidebar 2025-12-18 00:02:16 +08:00
Brendan Allan
382ec8fb2c tauri: update nsis icon 2025-12-17 23:40:52 +08:00
Stoufiler
6454adcd69 docs: Sort LSP Server list (#5688) 2025-12-17 09:14:26 -06:00
Rohan Mukherjee
99548554d7 feat: added lucent-orng theme (#5678)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-12-17 09:10:35 -06:00
Sachnun
751899eeec fix: remove unsupported parameter from bash tool description (#5676) 2025-12-17 08:59:42 -06:00
Brendan Allan
f8df1d3185 tauri: return after update failures 2025-12-17 22:54:54 +08:00
GitHub Action
b07a47fc89 chore: format code 2025-12-17 14:52:05 +00:00
Brendan Allan
c6f84f32d7 tauri: only alert on update failure when triggered manually 2025-12-17 22:51:14 +08:00
Brendan Allan
ebe25c3e9a tauri: dev icons + separate prod config (#5691)
Co-authored-by: GitHub Action <action@github.com>
2025-12-17 22:23:03 +08:00
Adam
65d7fc3ccd fix: command shortcuts 2025-12-17 07:36:53 -06:00
Github Action
4f3037d803 Update Nix flake.lock and hashes 2025-12-17 13:35:05 +00:00
Amadeus Demarzi
5c490c51ed Diffs Performance Improvements (#5653)
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
2025-12-17 07:33:46 -06:00
GitHub Action
5da1c0087b ignore: update download stats 2025-12-17 2025-12-17 12:04:41 +00:00
David Hill
4375149e63 wip: auto-detect OS and show desktop download button 2025-12-17 11:43:04 +00:00
David Hill
b695d3b6bb fix: website cta button 2025-12-17 11:21:57 +00:00
Adam
d7e133732c chore: cleanup 2025-12-17 03:58:16 -06:00
Adam
494e6fff01 feat(desktop): share sessions 2025-12-17 03:47:49 -06:00
Adam
0c7a297b1d feat(desktop): lsp diagnostics displayed 2025-12-17 03:47:48 -06:00
Github Action
9b1f9007c3 Update Nix flake.lock and hashes 2025-12-17 08:41:07 +00:00
GitHub Action
34ef5f4ece chore: format code 2025-12-17 08:40:22 +00:00
Sebastian Herrlinger
73ad20b90c upgrade opentui to v0.1.61 2025-12-17 09:38:04 +01:00
Jeon Suyeol
340e80257a Add availability to disable terminal title using OPENCODE_DISABLE_TERMINAL_TITLE env (#5661) 2025-12-16 23:57:03 -06:00
Dax Raad
c23ea2a211 ci: update publish workflow configuration 2025-12-16 21:01:44 -05:00
GitHub Action
a5f964aec6 chore: format code 2025-12-17 01:28:42 +00:00
Spoon
b8a8fb0de6 plugin(hook): add task tool execution hooks and command context tracking (#5642) 2025-12-16 19:28:09 -06:00
Aiden Cline
a6a8f41fd3 ci: tweak triage 2025-12-16 19:27:44 -06:00
Matt Silverlock
c137babea3 github: add configurable mentions input (#5655) 2025-12-16 19:14:50 -06:00
Adam
db2abc1b2c tui: increase session width to accommodate longer code blocks and improve readability 2025-12-16 17:31:26 -06:00
David Hill
a0f9f8dabb fix: load more button 2025-12-16 23:23:18 +00:00
Aiden Cline
8a185aa678 ci: fix missing pkg issue 2025-12-16 16:31:46 -06:00
Dax Raad
29aaf4f000 ci: fix release draft configuration to prevent automatic draft flag 2025-12-16 17:17:06 -05:00
GitHub Action
fc940dfcfb chore: format code 2025-12-16 22:07:10 +00:00
Adam
2f2ea98937 fix(share): content wasn't centered 2025-12-16 16:06:28 -06:00
opencode
ef0fa2007b release: v1.0.164 2025-12-16 21:47:41 +00:00
Adam
f07d4b933c fix(desktop): prompt history nav 2025-12-16 15:42:35 -06:00
Aiden Cline
5f57cee8e4 fix: user invoked subtasks causing tool_use or missing thinking signa… (#5650) 2025-12-16 15:42:21 -06:00
Adam
1755a3fe07 fix(desktop): auto-scroll 2025-12-16 15:32:14 -06:00
Adam
99680baf83 fix(desktop): focus prompt input after dialog close 2025-12-16 15:25:00 -06:00
Adam
9aa5460a0e fix(desktop): prompt history navigation 2025-12-16 15:10:44 -06:00
Adam
b4014e5baa fix: auto-scroll 2025-12-16 15:10:43 -06:00
Adam
96e4dcb521 fix: working logic 2025-12-16 15:10:43 -06:00
Adam
7e682a95c4 fix: prompt input multi line input 2025-12-16 15:10:43 -06:00
Adam
5eeba76bc5 fix: defensive audio init 2025-12-16 15:10:43 -06:00
Eric Guo
a2c91ebc32 feat(desktop): Loading more session number per project by button (#5616)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-16 14:50:33 -06:00
matvey
1aee8b49e1 feat: add experimental oxfmt formatter (#5620) 2025-12-16 14:43:14 -06:00
Aiden Cline
984f17ddd7 ci: include desktop & tauri in release notes 2025-12-16 14:37:33 -06:00
Aiden Cline
d556143e3b ci: fix branch name 2025-12-16 14:35:42 -06:00
Aiden Cline
7e3ad770ac fix: git branch filewatcher, add flag to completely disable watcher 2025-12-16 14:31:09 -06:00
Aiden Cline
87524de265 ci: fix triage 2025-12-16 14:15:50 -06:00
Aiden Cline
ee10d9b898 ci: auto tag github action once a change is shipped for it 2025-12-16 14:13:51 -06:00
Dax Raad
bbd36e8441 core: update plugin dependency and config loading for .opencode directory support 2025-12-16 15:06:50 -05:00
Dax Raad
4e2d1acf7d core: fix Tauri desktop app SSE connection timeout
- Add heartbeat events to /global/event and /event SSE endpoints
- Send server.heartbeat event every 30s to prevent WKWebView 60s timeout
- Fixes desktop app disconnecting from global events after 1 minute
2025-12-16 14:45:03 -05:00
Dax Raad
40d63cd1e3 fix 2025-12-16 14:25:12 -05:00
Dax Raad
77b2331428 ignore: update opencode plugin dependency 2025-12-16 14:21:19 -05:00
Dax Raad
2b7e2edee5 core: ensure desktop app loads user shell environment variables
Changes shell spawn flags from -l to -il so that ~/.zshrc and
~/.bashrc are sourced when starting the desktop app on macOS
and Linux. This fixes missing PATH and other environment
variables that users expect to be available.
2025-12-16 14:21:19 -05:00
Adam
28aba35ff9 feat(desktop): show retries 2025-12-16 13:19:32 -06:00
Adam
89219a77f7 fix: layout badness 2025-12-16 12:53:12 -06:00
Adam
20e3a74bad fix: defensive audio init 2025-12-16 12:53:12 -06:00
Adam
ff690350b1 feat(desktop): show write tool output 2025-12-16 12:53:11 -06:00
Adam
ebefb26e8f chore: cleanup 2025-12-16 12:53:11 -06:00
Dax Raad
0b1ee9ddd9 Merge remote-tracking branch 'origin/dev' into dev 2025-12-16 13:52:50 -05:00
Dax Raad
79599f351e chore: update opencode plugin dependencies and fix tauri sidecar path 2025-12-16 13:52:40 -05:00
Github Action
8c9f6b1d3e Update Nix flake.lock and hashes 2025-12-16 18:38:59 +00:00
Dax Raad
83bcb9e95b tui: fix autocomplete file loading and update dependencies 2025-12-16 13:37:22 -05:00
David Hill
96b9ff8d0e fix: remove the selected state from button when select deselected 2025-12-16 17:41:56 +00:00
David Hill
0af2254856 wip: add active state to open select 2025-12-16 17:35:04 +00:00
Fran Zekan
c2944024a8 fix: enable shell alias expansion in ! command (#5621) 2025-12-16 11:32:31 -06:00
GitHub Action
5be4bda90f chore: format code 2025-12-16 17:30:08 +00:00
shekohex
b78e2db013 docs: fix typo in Google Antigravity github link (#5625) 2025-12-16 11:29:31 -06:00
jinzhongjia
3f4d1121a4 docs: Add new project entry for opencode.nvim frontend (#5626) 2025-12-16 11:29:06 -06:00
Connor Adams
def910021d docs: add homebrew command for opencode-desktop (#5631)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-16 11:28:40 -06:00
Aiden Cline
3ac42e9632 fix: github install cmd if repo has . in it 2025-12-16 11:19:18 -06:00
David Hill
9c26bb7c6c fix: breadcrumb dropdown position left aligned 2025-12-16 17:16:51 +00:00
David Hill
53f20f7612 Revert "wip: make the default container wider"
This reverts commit 1f18f389c0.
2025-12-16 17:16:51 +00:00
Tommy D. Rossi
11b3927dc2 fix: use system prompt field from prompt input (#5633)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-16 10:52:22 -06:00
David Hill
a190eda2c8 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-12-16 16:36:15 +00:00
David Hill
1f18f389c0 wip: make the default container wider 2025-12-16 16:35:57 +00:00
GitHub Action
84e56ee614 chore: format code 2025-12-16 16:10:36 +00:00
Aiden Cline
59329a414d ci: tweak triage 2025-12-16 10:09:51 -06:00
Brendan Allan
452c991f58 Keep release a draft until all builds are finished (#5632)
Co-authored-by: opencode <opencode@sst.dev>
Co-authored-by: GitHub Action <action@github.com>
2025-12-16 23:34:44 +08:00
Simon D'Morias
be8116e2ea fix: preserve argument boundaries in run command (#4979) 2025-12-16 06:21:51 -06:00
David Hill
f0ed1e38c9 Revert "fix: strip parentheses from file paths generated by llm"
This reverts commit 6c1a1a77b7.
2025-12-16 12:12:01 +00:00
GitHub Action
ac0f1dbbdd ignore: update download stats 2025-12-16 2025-12-16 12:04:54 +00:00
GitHub Action
275a352e81 chore: format code 2025-12-16 11:51:08 +00:00
David Hill
9f3bc0e352 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-12-16 11:50:25 +00:00
David Hill
6c1a1a77b7 fix: strip parentheses from file paths generated by llm 2025-12-16 11:50:23 +00:00
David Hill
2e21c62320 fix: font size updates 2025-12-16 11:48:52 +00:00
David Hill
19c6fec4d1 wip: font-size updates 2025-12-16 11:12:08 +00:00
Brendan Allan
4779d99a13 tauri: explicitly kill sidecar before updater relaunch 2025-12-16 19:01:37 +08:00
David Hill
05e0759878 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-12-16 10:54:51 +00:00
David Hill
2330ec6dc3 fix: font size updates 2025-12-16 10:54:50 +00:00
GitHub Action
75e5130cf8 chore: format code 2025-12-16 10:33:49 +00:00
Brendan Allan
87efd27459 tauri: macos-only app menu 2025-12-16 18:33:04 +08:00
Aiden Cline
62f080b0e4 fix: small bug w/ install script 2025-12-16 00:20:05 -06:00
Aiden Cline
ae3990a557 chore: centralize dep to catalog & fix typos 2025-12-15 23:07:55 -06:00
Aiden Cline
d7b5b431d6 ci: Update issue assignment and labeling guidelines
Clarified assignment responsibilities and labeling for issues related to OpenCode tools.
2025-12-15 22:11:30 -06:00
Dax Raad
e2fbd098d2 tui: fix dialog select items taking up 2 lines when truncated
Prevents text wrapping in dialog select options by removing wrapMode,
ensuring truncated text stays on single line and maintains proper timestamp visibility
2025-12-15 22:57:52 -05:00
Luke Parker
ef78fd8bae fix: debounce LSP diagnostics to get complete results (#5600) 2025-12-15 21:26:59 -06:00
DS
72ebaeb8f7 fix: rejoin system prompt if experimental plugin hook triggers to preserve caching (#5550)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-15 20:00:26 -06:00
Lucas Duailibe
0dc62d5dad make install script use tmp dir (#5601) 2025-12-15 19:58:07 -06:00
Aiden Cline
d118782a10 ci: cheaper model 2025-12-15 19:24:30 -06:00
Aiden Cline
ff05647350 ci: ignore 2025-12-15 19:18:39 -06:00
GitHub Action
0e1c711c4e chore: format code 2025-12-16 01:16:20 +00:00
Aiden Cline
bfb254dac6 ci: auto triage issues 2025-12-15 19:15:40 -06:00
opencode
92fe927785 release: v1.0.162 2025-12-16 00:40:27 +00:00
GitHub Action
2e25fe9d5d chore: format code 2025-12-16 00:36:01 +00:00
Dax Raad
38c5f23f4a tui: update dialog context and server to use new single dialog system 2025-12-15 19:35:19 -05:00
Dax Raad
112c58abf5 tui: refactor dialog system to use single active dialog instead of stack 2025-12-15 19:35:18 -05:00
opencode
0dce5173cc release: v1.0.161 2025-12-16 00:17:15 +00:00
Adam
2c70c0b00f fix: undefined events 2025-12-15 18:13:11 -06:00
opencode-agent[bot]
34024c2504 docs: models --refresh flag in cli.mdx (#5596)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-12-15 18:04:47 -06:00
Luke Parker
27e826eba6 fix(win32): Normalise LSP paths on windows (fixes lua) (#5597) 2025-12-15 18:01:03 -06:00
Aiden Cline
89a4f1c1ae tweak: add .catch for extractZip calls 2025-12-15 17:41:28 -06:00
GitHub Action
c0c61b25ff chore: format code 2025-12-15 23:30:06 +00:00
Luke Parker
0d1c6e0ca9 fix(win32): Missing LSP can now unzip on windows (#5594) 2025-12-15 17:29:30 -06:00
opencode
002db3abf4 release: v1.0.160 2025-12-15 23:26:00 +00:00
Dax Raad
416a919c6d tui: fix dialog replacement to prevent nested dialogs from showing simultaneously 2025-12-15 18:21:54 -05:00
Dax Raad
dbbcf0b8d0 tui: fix model selection dialog to properly replace current dialog instead of creating nested dialogs 2025-12-15 18:14:33 -05:00
Luke Parker
efac8cebb3 fix(win32): correct ElixirLS extension typo (#5590) 2025-12-15 16:47:27 -06:00
GitHub Action
4f2baf1a72 chore: format code 2025-12-15 22:40:18 +00:00
Luke Parker
48b2bde6e5 fix(win32): use path.delimiter for PATH separator in LSP server lookups (#5589) 2025-12-15 16:39:42 -06:00
opencode
88314148e6 release: v1.0.159 2025-12-15 22:19:08 +00:00
Dax Raad
56452d886d fix dialog root complexity 2025-12-15 17:13:20 -05:00
Luke Parker
f3e64cfb19 fix(windows): opencode github install (#5587) 2025-12-15 16:12:54 -06:00
Ariane Emory
8fcc80bc20 fix: restore ability to bind keys for model_cycle_favorite model_cycle_favorite_reverse (resolves #5198) (#5202) 2025-12-15 16:03:50 -06:00
opencode-agent[bot]
0beccc406e docs: enabled_providers docs section (#5586)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-15 16:01:35 -06:00
Adam
b82ea693db fix: multiline user input 2025-12-15 15:22:39 -06:00
Adam
4fd9a19fbb fix: keybinds for agent and model selection 2025-12-15 15:19:19 -06:00
Adam
e16487b804 fix: landing page CLS hero jump down 2025-12-15 15:10:48 -06:00
Adam
5388192aac fix: session nav on homepage 2025-12-15 15:10:17 -06:00
Adam
8010448ba1 fix: default steps expanded unless done 2025-12-15 15:03:00 -06:00
Adam
66f3e69867 fix: landing page CLS hero jump down 2025-12-15 14:59:24 -06:00
Aiden Cline
ca599ab8fc tweak: add model flag support for agent create command 2025-12-15 14:55:59 -06:00
Jay V
c3b3b133b0 docs: make project names clickable links in ecosystem documentation 2025-12-15 15:44:46 -05:00
Adam
300ec0e0af fix: missing event type (global) 2025-12-15 14:43:48 -06:00
Ariane Emory
6632987827 fix: record shell mode in history (resolves #5454) (#5551) 2025-12-15 14:42:17 -06:00
GitHub Action
e555e893c4 chore: format code 2025-12-15 20:35:39 +00:00
Aiden Cline
81134cf61e add ability to set topK 2025-12-15 14:34:56 -06:00
opencode
37e4c1e619 release: v1.0.158 2025-12-15 20:28:44 +00:00
Adam
02b5e7d72c fix: modal search 2025-12-15 14:25:19 -06:00
justfortheloveof
7abc2a947e tweak: prioritize fuzzysort results that start with user input (#5571) 2025-12-15 14:22:37 -06:00
Adam
337a7e9646 fix: allow for non-vcs projects in desktop 2025-12-15 14:16:08 -06:00
GitHub Action
62cc532ecc chore: format code 2025-12-15 20:09:08 +00:00
Dax Raad
d5a506d4ae core: fix server response handling to prevent connection timeouts 2025-12-15 15:07:20 -05:00
opencode
9c5f94bd66 release: v1.0.157 2025-12-15 19:46:23 +00:00
Dax Raad
83390314d6 ci: fix tauri updater version mismatch by checking out the release tag 2025-12-15 14:42:34 -05:00
Dax Raad
8b08e9cda5 ci: add app bundle target to fix macOS updater by generating .app.tar.gz files 2025-12-15 14:41:38 -05:00
GitHub Action
b1b1df824c chore: format code 2025-12-15 19:33:56 +00:00
Dax Raad
7d1733c752 core: fix message caching for Anthropic models to improve response consistency 2025-12-15 14:33:14 -05:00
opencode
cf05e6e02b release: v1.0.156 2025-12-15 19:28:02 +00:00
Adam
7e49d0fb15 fix: connect provider on homepage 2025-12-15 13:24:30 -06:00
Adam
c4f63824df fix: terminal in desktop 2025-12-15 13:09:13 -06:00
Adam
4236744fb5 fix: image attachments in desktop 2025-12-15 12:01:11 -06:00
Github Action
284c045795 Update Nix flake.lock and hashes 2025-12-15 16:57:28 +00:00
Dax Raad
2c53abd70c docs: update header navigation to include desktop download 2025-12-15 11:56:53 -05:00
Adam
b7a9cbfc68 fix: share page 2025-12-15 10:56:07 -06:00
Dax Raad
46a35dfc1b docs: restore desktop beta banner to homepage 2025-12-15 11:54:09 -05:00
GitHub Action
b7597c12dd chore: format code 2025-12-15 16:52:45 +00:00
Nalin Singh
6830590183 feat: add F# language server support (#5549) 2025-12-15 10:52:16 -06:00
opencode
b9b4349039 release: v1.0.155 2025-12-15 16:44:08 +00:00
Dax Raad
4107918909 20min 2025-12-15 11:31:38 -05:00
Adam
6347ee9988 chore: update stats 2025-12-15 10:31:11 -06:00
GitHub Action
9daa4e04ea chore: format code 2025-12-15 16:22:49 +00:00
Adam
ed96ae9d45 chore: cleanup 2025-12-15 10:22:07 -06:00
Adam
8ce0966987 wip(desktop): progress 2025-12-15 10:22:06 -06:00
Adam
8cb26b6066 wip(desktop): progress 2025-12-15 10:22:06 -06:00
Adam
5cf6a1343c wip(desktop): progress 2025-12-15 10:22:04 -06:00
Adam
44d6c5780d wip(desktop): progress 2025-12-15 10:20:20 -06:00
Adam
5eaa8e1bf4 chore: cleanup 2025-12-15 10:20:20 -06:00
Adam
df2713a6c2 chore: cleanup 2025-12-15 10:20:19 -06:00
Adam
ff6864a7ca feat(desktop): custom commands 2025-12-15 10:20:19 -06:00
Adam
5e37a902ce wip(desktop): progress 2025-12-15 10:20:19 -06:00
Adam
df2ebfac7d wip(desktop): progress 2025-12-15 10:20:19 -06:00
Adam
5fbcb203f5 wip(desktop): progress 2025-12-15 10:20:19 -06:00
Adam
34db739442 wip(desktop): progress 2025-12-15 10:20:18 -06:00
Adam
ae8c4154aa wip(desktop): progress 2025-12-15 10:20:18 -06:00
Adam
315836c0b7 wip(desktop): progress 2025-12-15 10:20:18 -06:00
Adam
c0d009d5f3 wip(desktop): progress 2025-12-15 10:20:18 -06:00
Adam
c36f3b9dbe wip(desktop): progress 2025-12-15 10:20:17 -06:00
Adam
d31824320e Revert "wip(desktop): session turn state consolidation"
This reverts commit 453f862616dc4d3ac90680581cde279e118b0da1.
2025-12-15 10:20:17 -06:00
Adam
88c0675148 wip(desktop): progress 2025-12-15 10:20:17 -06:00
Adam
82c4755fb0 wip(desktop): progress 2025-12-15 10:20:17 -06:00
Adam
40572eeba4 wip(desktop): progress 2025-12-15 10:20:17 -06:00
Adam
d81d63045a wip(desktop): session turn state consolidation 2025-12-15 10:20:16 -06:00
Adam
ece3bfd93d wip(desktop): progress 2025-12-15 10:20:16 -06:00
Adam
acd91bddf7 wip(desktop): progress 2025-12-15 10:20:16 -06:00
Adam
3a14ca044c wip(desktop): progress 2025-12-15 10:20:16 -06:00
Adam
d66d806700 wip(desktop): progress 2025-12-15 10:20:16 -06:00
Adam
e9b95b2e91 wip(desktop): progress 2025-12-15 10:20:15 -06:00
opencode
56dde2cc83 release: v1.0.154 2025-12-15 16:01:15 +00:00
Dax Raad
d2ce368a3f ci: update publish workflow concurrency to include version inputs and upgrade ubuntu runner to 24.04 2025-12-15 10:14:20 -05:00
GitHub Action
f492122d59 chore: format code 2025-12-15 14:59:05 +00:00
Dax Raad
b0f77da56c core: reorganize agent configuration to separate primary agents (build, plan) from subagents 2025-12-15 09:58:23 -05:00
Dax Raad
274b86b19b ci: fix AppImage build hanging by using portable appimage format
- Add appimage target back to tauri.conf.json
- Force reinstall tauri-cli from feat/truly-portable-appimage branch
- Add 10 minute timeout to prevent indefinite hangs
- Add logging to verify correct tauri-cli version is installed

This fixes the issue where AppImage builds would hang at 'Running input plugin: gtk' by using the new portable appimage format that bypasses linuxdeploy entirely.
2025-12-15 09:47:19 -05:00
René
2ca118db59 docs: Fix Wakatime repository link in ecosystem.mdx (#5552)
Co-authored-by: Github Action <action@github.com>
2025-12-15 08:32:06 -06:00
David Hill
a0c0e2b5c3 fix: add tooltip to close review tab 2025-12-15 14:22:57 +00:00
David Hill
d43fbec12d fix: prompt input using border shadow 2025-12-15 14:14:10 +00:00
David Hill
bb426112ed fix: replace agents dropdown with shadow border 2025-12-15 14:09:47 +00:00
David Hill
d2217bb825 fix: fix width and padding on agent select
- conditionally hide the role=presentation element because it was adding extra gap before the first agent
2025-12-15 14:05:24 +00:00
David Hill
ac495bd351 fix: hide prompt input send tooltip when the send button is disabled 2025-12-15 13:47:50 +00:00
David Hill
b913eb7acc fix: avatar and icon size in sidebar 2025-12-15 13:21:59 +00:00
David Hill
ea65a91b2e fix: remove blue border from prompt input 2025-12-15 13:11:09 +00:00
GitHub Action
ed6d749104 ignore: update download stats 2025-12-15 2025-12-15 12:05:05 +00:00
René
9eefcd1b41 Provider fix, anthropic Errorhandling if empty image file is read (#5521) 2025-12-14 23:56:47 -06:00
Nalin Singh
7c1124199e fix: input lip visibility for transparent themes (#5544) 2025-12-14 23:10:33 -06:00
Spoon
5cf126d489 fix(edit): add per-file lock to prevent read-before-write race (#4388) 2025-12-14 23:01:50 -06:00
Aiden Cline
509f7d9617 ignore: fix debug var in last commit 2025-12-14 22:59:30 -06:00
opencode-agent[bot]
ae1bf92c81 Add dismiss button to Getting Started box (#5543)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-14 22:58:26 -06:00
DS
b021b26e77 feat: restore experimental.chat.messages.transform and add experimental.chat.system.transform hooks (#5542) 2025-12-14 22:51:11 -06:00
Aiden Cline
9555d348de ci: switch model 2025-12-14 22:41:54 -06:00
Brendan Allan
220c564047 tauri: use correct sidecar name 2025-12-15 12:40:49 +08:00
Brendan Allan
cf5c0129ac tauri: rename sidecar to opencode-cli 2025-12-15 12:40:14 +08:00
Aiden Cline
543dbe71d2 ci: smart oc 2025-12-14 22:36:46 -06:00
Ravi Kumar
54569b5552 fix(session): fix unshare command not clearing share state (#5523) 2025-12-14 22:05:06 -06:00
GitHub Action
6a09861806 chore: format code 2025-12-15 03:42:56 +00:00
Adam
79a4c65313 fix: test 2025-12-14 21:42:17 -06:00
Adam
654534ac71 fix: update sdk 2025-12-14 21:41:20 -06:00
Adam
ba16bfdf3d wip(desktop): progress 2025-12-14 21:38:59 -06:00
Adam
ad5614bbb9 wip(desktop): progress 2025-12-14 21:38:59 -06:00
Adam
dda579c8ad wip(desktop): progress 2025-12-14 21:38:59 -06:00
Adam
4246cdb069 wip(desktop): progress 2025-12-14 21:38:58 -06:00
Adam
7ade6d386d wip(desktop): progress 2025-12-14 21:38:58 -06:00
Adam
2613f44961 wip(desktop): progress 2025-12-14 21:38:58 -06:00
Adam
62ffeb3987 fix(desktop): auto scroll 2025-12-14 21:38:58 -06:00
Adam
4a8e8f537c wip(desktop): progress 2025-12-14 21:38:58 -06:00
Adam
a68bee7878 fix(desktop): layout fixes 2025-12-14 21:38:57 -06:00
Mark Jaquith
ed33d82535 feat(cli): auto-submit prompt when using --prompt flag (#4510) 2025-12-14 21:06:04 -06:00
Dax Raad
2d63c22d1a fix share link 2025-12-14 22:05:53 -05:00
Dax Raad
e22af25076 ci: fix test failures in CI by pre-populating models cache
Tests were failing in CI because the models.json cache file doesn't exist
and the data() macro fallback only works at build time, not runtime.
The preload now pre-fetches models.json and disables the background
refresh to prevent race conditions during test execution.
2025-12-14 21:23:45 -05:00
Dax Raad
622caae9c9 fix desktop updater 2025-12-14 21:12:38 -05:00
Dax
fed4776451 LLM cleanup (#5462)
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-14 21:11:30 -05:00
Ravi Kumar
fdf560c343 fix(tui): --continue selects wrong session (#5513) 2025-12-14 19:50:54 -06:00
GitHub Action
fc3ffb2bf9 chore: format code 2025-12-14 23:14:05 +00:00
Martijn Baay
7368342bab feat: add experimental.continue_loop_on_deny config option (#4729)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2025-12-14 17:13:32 -06:00
Dax Raad
c8fc910533 ignore: simplify download page to use GitHub latest redirect URLs 2025-12-14 17:10:59 -05:00
GitHub Action
0f9ef84d55 chore: format code 2025-12-14 22:05:28 +00:00
Dax Raad
74b5c285cf disable app image 2025-12-14 17:04:46 -05:00
opencode
a34e67b518 release: v1.0.153 2025-12-14 19:04:01 +00:00
Aiden Cline
0c7f0cfa2e tweak: fallback to provider default for temperature 2025-12-14 11:57:12 -06:00
GitHub Action
10ee6d345b chore: format code 2025-12-14 17:48:34 +00:00
Nalin Singh
48ec68730f fix: ensure input borders are drawn in transparent themes (#5524) 2025-12-14 11:48:02 -06:00
GitHub Action
70e4efe429 chore: format code 2025-12-14 17:46:46 +00:00
Sellers Crisp
92948ed8a4 feat: add server_error, rate_limit, and no_kv_space retry logic to accommodate Foundry API issues (#5527)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-14 11:46:14 -06:00
shekohex
6d412d8872 docs: add opencode-pty and opencode-google-antigravity-auth plugins to the echosystem (#5530) 2025-12-14 11:38:53 -06:00
Lawrence Sarpong
e6a0a005d6 Add Gleam LSP and formatter (#5514) 2025-12-14 10:51:08 -06:00
Aymvn
90d44751e7 docs: Fix Wakatime link in ecosystem documentation (#5528) 2025-12-14 10:49:35 -06:00
GitHub Action
4d062ba1b2 ignore: update download stats 2025-12-14 2025-12-14 12:04:18 +00:00
Aiden Cline
f8bca50f00 rm unnecessary code 2025-12-13 23:45:15 -06:00
Aiden Cline
3d2ef28fa8 add topK function to transform, add temp defaults for glm and minimax 2025-12-13 23:27:11 -06:00
YeonGyu-Kim
210b3e905b fix(ui): guard Node reference for SSR compatibility in isTriggerTitle (#5509) 2025-12-13 22:28:14 -06:00
Brendan Allan
96975ef8d6 tauri: change mainBinaryName to just OpenCode 2025-12-14 12:08:53 +08:00
Brendan Allan
b8b998be56 tauri: bring back appimage 2025-12-14 12:00:14 +08:00
Sachnun
d8ac35f6e5 fix(tui): open parent session instead of subagent on continue flag (#5503) 2025-12-13 21:09:42 -06:00
Zhou Rui
ed1eacfce0 docs: Add opencode-websearch-cited to plugin list (#5501) 2025-12-13 20:28:39 -06:00
Adam
629f475f63 fix: sort models 2025-12-13 20:25:24 -06:00
Adam
43a7c1dd8c fix: use opencode icon 2025-12-13 20:25:24 -06:00
Adam
e288ce0fca chore: cleanup 2025-12-13 20:25:24 -06:00
Adam
67b3fcb31a chore: cleanup 2025-12-13 20:25:24 -06:00
Tommy D. Rossi
aedb5550a8 fix: limit LSP diagnostics to prevent context window waste (#5480)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-13 19:56:26 -06:00
Aiden Cline
1638ffde69 docs: networking 2025-12-13 18:20:44 -06:00
Aiden Cline
d4cfbd8219 chore: reduce duplication of field in transform 2025-12-13 18:07:22 -06:00
Adam
c7bac83212 chore: cleanup 2025-12-13 16:17:32 -06:00
Adam
fc9789d7a7 fix(desktop): archive button 2025-12-13 16:14:31 -06:00
Adam
a8957d8d16 fix(desktop): auto scroll 2025-12-13 15:56:12 -06:00
Adam
0660433921 feat(desktop): show richer status when thinking 2025-12-13 15:47:50 -06:00
Adam
1a6f4f1c0d fix: css scroll jitter 2025-12-13 15:36:28 -06:00
Adam
974a24ba02 fix: don't rotate placeholders in session 2025-12-13 15:25:56 -06:00
Adam
5ebe29de1e fix: don't open shell by default 2025-12-13 15:17:22 -06:00
Github Action
6bdf8b1fe1 Update Nix flake.lock and hashes 2025-12-13 21:13:53 +00:00
Adam
5bcc93851c chore: cleanup 2025-12-13 15:12:41 -06:00
Adam
d0789632b4 fix(desktop): terminal light mode 2025-12-13 15:12:32 -06:00
Adam
a6e297baad feat(desktop): message history 2025-12-13 14:57:24 -06:00
Adam
307af10c8b fix: session turn scroll 2025-12-13 14:57:23 -06:00
Felipe Oduardo Sierra
f254cf76d9 add ARM64 Docker image support (#5483) 2025-12-13 13:01:59 -06:00
Github Action
b4ffaa21ec Update Nix flake.lock and hashes 2025-12-13 19:01:20 +00:00
Aiden Cline
7bf6f264e4 bump bun version & set flags this time 2025-12-13 13:00:03 -06:00
GitHub Action
7434fbba8e chore: format code 2025-12-13 17:34:07 +00:00
Jan-Niklas W.
b7581e01ea docs: fix title for JetBrains ACP config file (#5479) 2025-12-13 11:33:31 -06:00
YeonGyu-Kim
b46d4789fc docs: add oh-my-opencode to plugins list (#5481) 2025-12-13 11:33:10 -06:00
GitHub Action
199bd8a9a2 chore: format code 2025-12-13 17:30:48 +00:00
rari404
decf2452c4 feat: add dockerfile language server (#5252)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-13 11:30:15 -06:00
GitHub Action
d8663a44c2 ignore: update download stats 2025-12-13 2025-12-13 12:04:19 +00:00
rari404
8917a4c609 feat: add texlab language server and latexindent formatter (#5251) 2025-12-12 23:50:09 -06:00
GitHub Action
5d7a52f8b8 chore: format code 2025-12-13 02:09:41 +00:00
Jan-Niklas W.
b7b827c5bd docs: JetBrains IDEs to ACP config docs page (#5465) 2025-12-12 20:09:08 -06:00
Matt Silverlock
613e082358 github: support GITHUB_TOKEN + skip OIDC (#5459) 2025-12-12 19:55:46 -06:00
Charles Cooper
b6856bd593 fix: add --session flag to attach command (#5460)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2025-12-12 19:45:28 -06:00
David Hill
7cb5a77ba6 fix: mute the project path in the sidebar that proceeds the final directory 2025-12-12 23:45:39 +00:00
David Hill
cd9898a565 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-12-12 23:25:11 +00:00
Dax Raad
a4ffa869cc fix 2025-12-12 18:15:31 -05:00
David Hill
dbc84ff4c3 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-12-12 22:50:50 +00:00
David Hill
c11ea3fd92 fix: mute the whole prompt area when leader key is active 2025-12-12 22:50:48 +00:00
GitHub Action
3c3a0f8afb chore: format code 2025-12-12 22:48:43 +00:00
Aiden Cline
b93614cb81 docs: add env vars sections 2025-12-12 16:47:50 -06:00
opencode
b84d513bd7 release: v1.0.152 2025-12-12 22:29:21 +00:00
Adam
0554d03162 Revert "fix: archive button"
This reverts commit bc3286de46.
2025-12-12 16:16:52 -06:00
Aiden Cline
15caecdb45 shell tweaks, better handling for windows (#5455)
Co-authored-by: GitHub Action <action@github.com>
2025-12-12 16:11:07 -06:00
Adam
91ab966921 fix: max height on bash tool 2025-12-12 16:10:13 -06:00
Adam
bc3286de46 fix: archive button 2025-12-12 16:03:07 -06:00
Dax Raad
af45444496 desktop: fix build on Linux and Windows by making macOS title bar styling conditional 2025-12-12 16:47:48 -05:00
Sebastian Herrlinger
43202f2820 only exit app when prompt is empty, otherwise fallthrough, fix #5457 2025-12-12 22:45:28 +01:00
GitHub Action
ce37e11bfe chore: format code 2025-12-12 21:44:09 +00:00
Dan Brown
6e9833acce Shell: No -l in fallback, for max compatibility (#5452) 2025-12-12 15:43:35 -06:00
opencode
379c4ecab3 release: v1.0.151 2025-12-12 21:34:32 +00:00
Github Action
f1db4b60c4 Update Nix flake.lock and hashes 2025-12-12 21:28:16 +00:00
Adam
9846b26be7 fix: desktop layout 2025-12-12 15:26:53 -06:00
Adam
d6ba6af6f3 fix: desktop layout 2025-12-12 15:24:43 -06:00
Adam
d463ade028 fix: desktop layout 2025-12-12 15:24:43 -06:00
Adam
6c3495a75a fix: desktop layout 2025-12-12 15:24:43 -06:00
Adam
a16edb4ea0 fix: desktop layout 2025-12-12 15:24:43 -06:00
Adam
9efe09564b fix: desktop layout 2025-12-12 15:24:42 -06:00
Adam
ccdd77032a fix: desktop layout 2025-12-12 15:24:42 -06:00
Adam
41e234c6d0 fix: desktop layout 2025-12-12 15:24:42 -06:00
Adam
3e03646e42 fix: desktop layout 2025-12-12 15:24:42 -06:00
Adam
f7acc34327 fix: desktop layout 2025-12-12 15:24:41 -06:00
Adam
bf420e7df6 chore: cleanup 2025-12-12 15:24:41 -06:00
Adam
78484f545c chore: cleanup 2025-12-12 15:24:41 -06:00
Adam
ad008d2151 wip: desktop timeline changes 2025-12-12 15:24:41 -06:00
Dax Raad
651a10d6db only gen summary if diffs 2025-12-12 16:24:26 -05:00
Aiden Cline
f9674793af tweak: 5.1 -> 5. in transform 2025-12-12 15:17:11 -06:00
GitHub Action
f3a33d41f1 chore: format code 2025-12-12 20:26:02 +00:00
David Hill
642eec3dfd Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-12-12 20:25:21 +00:00
Dax Raad
73513612d4 reuse existing server query 2025-12-12 15:23:38 -05:00
David Hill
9b77246246 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-12-12 20:23:06 +00:00
David Hill
cf3bc1e0a6 fix: increase font-size-small to 13px 2025-12-12 20:22:51 +00:00
David Hill
4550ad049e fix: make syntax colors have more contrast 2025-12-12 20:22:08 +00:00
Dax Raad
d51c6ca39f sync 2025-12-12 15:21:29 -05:00
Dax Raad
47c6a2430c sync 2025-12-12 15:18:07 -05:00
Aiden Cline
909013320b tweak: 5.1 -> 5. reasoning effort match 2025-12-12 13:21:57 -06:00
Frank
770a4d87db Zen: add gpt5.2 2025-12-12 13:48:54 -05:00
Luke Parker
2e417c4d8c fix: osascript for clipboard typo (#5430) 2025-12-12 11:37:43 -06:00
Adam
2da527aaa6 fix: desktop layout 2025-12-12 11:11:17 -06:00
Jeremy Osih
0303eb0cc1 Change tooltip text from 'Open file' to 'New Terminal' (#5435) 2025-12-12 10:57:52 -06:00
Adam
3f4a792c8a fix: tweak missing colors 2025-12-12 10:56:30 -06:00
Adam
9629f7464b chore: cleanup 2025-12-12 10:45:34 -06:00
xu0o0
9017d10303 acp: fix internal error on /compact (#5424) 2025-12-12 10:43:18 -06:00
Brendan Allan
59d4041aa4 tauri: add basic custom titlebar (#5438) 2025-12-12 09:37:17 -06:00
Adam
feb8c4f3c6 feat(desktop): archive sessions 2025-12-12 07:07:54 -06:00
Adam
3f5cd2c4a8 fix(desktop): audio stuff 2025-12-12 06:21:31 -06:00
Adam
a160eb76df fix(desktop): homedir aware path on home 2025-12-12 06:21:31 -06:00
GitHub Action
e4b2028f91 ignore: update download stats 2025-12-12 2025-12-12 12:04:34 +00:00
Github Action
ffc48e115b Update Nix flake.lock and hashes 2025-12-12 11:16:22 +00:00
Adam
04b4dacee3 feat(desktop): basic alerting 2025-12-12 05:14:51 -06:00
GitHub Action
c0e30f48c6 chore: format code 2025-12-12 09:44:46 +00:00
David Hill
99158e736b Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-12-12 09:44:06 +00:00
David Hill
4c02d515a1 wip: desktop sidebar icon updates 2025-12-12 09:44:04 +00:00
David Hill
b803a9732d fix: make the logo on the home screen non-selectable 2025-12-12 09:43:41 +00:00
Brendan Allan
f9d5e18790 tauri: create window with full screen size 2025-12-12 17:04:00 +08:00
Github Action
147111c9c6 Update Nix flake.lock and hashes 2025-12-12 09:00:19 +00:00
GitHub Action
9a70eb538b chore: format code 2025-12-12 08:59:18 +00:00
Brendan Allan
0b1731142e tauri: initialise store and window-state plugins 2025-12-12 16:58:37 +08:00
Viktor Forsman
7ec48dfd15 fix: debug lsp diagnostics cmd for certain lsps (#5420) 2025-12-11 23:20:38 -06:00
Frank
57120e69ed Zen: sync 2025-12-11 23:41:04 -05:00
Rhys Sullivan
11efda3f5c [feat]: show indicator for in progress chats in the sessions list (#5417) 2025-12-11 21:57:03 -05:00
Sachnun
a5cb4e41f5 fix(tui): restore input on timeline revert and show newest first (#5366) 2025-12-11 20:07:31 -06:00
GitHub Action
88b2382b97 chore: format code 2025-12-12 02:06:49 +00:00
Sachnun
237c0253c2 fix(server): make time field optional in session update validator (#5372) 2025-12-11 20:06:15 -06:00
xu0o0
a9f27371cf acp: replay conversation history in session/load (#5385) 2025-12-11 20:02:06 -06:00
opencode-agent[bot]
9c126c5b64 Removed cache mention from webfetch prompt. (#5412)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2025-12-11 19:53:39 -06:00
Zeno Jiricek
e241aa21b9 docs: Add opencode-md-table-formatter and plugin template (#5405) 2025-12-11 17:02:49 -06:00
Koichi Nakayamada
1fd005838c fix(tui): ensure fatal error UI is readable in light mode (#5387) 2025-12-11 16:39:27 -06:00
386 changed files with 22135 additions and 5777 deletions

View File

@@ -5,6 +5,9 @@ on:
- cron: "0 */12 * * *"
workflow_dispatch:
env:
LOOKBACK_HOURS: 4
jobs:
update-docs:
if: github.repository == 'sst/opencode'
@@ -25,9 +28,9 @@ jobs:
- name: Get recent commits
id: commits
run: |
COMMITS=$(git log --since="4 hours ago" --pretty=format:"- %h %s" 2>/dev/null || echo "")
COMMITS=$(git log --since="${{ env.LOOKBACK_HOURS }} hours ago" --pretty=format:"- %h %s" 2>/dev/null || echo "")
if [ -z "$COMMITS" ]; then
echo "No commits in the last 4 hours"
echo "No commits in the last ${{ env.LOOKBACK_HOURS }} hours"
echo "has_commits=false" >> $GITHUB_OUTPUT
else
echo "has_commits=true" >> $GITHUB_OUTPUT
@@ -47,7 +50,7 @@ jobs:
model: opencode/gpt-5.2
agent: docs
prompt: |
Review the following commits from the last 4 hours and identify any new features that may need documentation.
Review the following commits from the last ${{ env.LOOKBACK_HOURS }} hours and identify any new features that may need documentation.
<recent_commits>
${{ steps.commits.outputs.list }}

View File

@@ -64,7 +64,7 @@ jobs:
Please check all the code changes in this pull request against the style guide, also look for any bugs if they exist. Diffs are important but make sure you read the entire file to get proper context. Make it clear the suggestions are merely suggestions and the human can decide what to do
When critiquing code against the style guide, be sure that the code is ACTUALLY in violation, don't complain about else statements if they already use early returns there. You may complain about excessive nesting though, regardless of else statement usage.
When critiquing code style don't be a zealot, we don't like "let" statements but sometimes they are the simpliest option, if someone does a bunch of nesting with let, they should consider using iife (see packages/opencode/src/util.iife.ts)
When critiquing code style don't be a zealot, we don't like "let" statements but sometimes they are the simplest option, if someone does a bunch of nesting with let, they should consider using iife (see packages/opencode/src/util.iife.ts)
Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block.
If you are writing suggested fixes, BE SURE THAT the change you are recommending is actually valid typescript, often I have seen missing closing "}" or other syntax errors.

33
.github/workflows/stale-issues.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: "Auto-close stale issues"
on:
schedule:
- cron: "30 1 * * *" # Daily at 1:30 AM
workflow_dispatch:
env:
DAYS_BEFORE_STALE: 90
DAYS_BEFORE_CLOSE: 7
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/stale@v10
with:
days-before-stale: ${{ env.DAYS_BEFORE_STALE }}
days-before-close: ${{ env.DAYS_BEFORE_CLOSE }}
stale-issue-label: "stale"
close-issue-message: |
[automated] Closing due to ${{ env.DAYS_BEFORE_STALE }}+ days of inactivity.
Feel free to reopen if you still need this!
stale-issue-message: |
[automated] This issue has had no activity for ${{ env.DAYS_BEFORE_STALE }} days.
It will be closed in ${{ env.DAYS_BEFORE_CLOSE }} days if there's no new activity.
remove-stale-when-updated: true
exempt-issue-labels: "pinned,security,feature-request,on-hold"
start-date: "2025-12-27"

View File

@@ -32,3 +32,4 @@ jobs:
./script/sync-zed.ts ${{ steps.get_tag.outputs.tag }}
env:
ZED_EXTENSIONS_PAT: ${{ secrets.ZED_EXTENSIONS_PAT }}
ZED_PR_PAT: ${{ secrets.ZED_PR_PAT }}

View File

@@ -2,11 +2,9 @@ name: test
on:
push:
branches-ignore:
- production
branches:
- dev
pull_request:
branches-ignore:
- production
workflow_dispatch:
jobs:
test:

4
.gitignore vendored
View File

@@ -20,3 +20,7 @@ opencode.json
a.out
target
.scripts
# Local dev files
opencode-dev
logs/

View File

@@ -1,5 +1,6 @@
---
description: ALWAYS use this when writing docs
color: "#38A3EE"
---
You are an expert technical documentation writer

View File

@@ -1,10 +0,0 @@
---
description: Use this agent when you are asked to commit and push code changes to a git repository.
mode: subagent
---
You commit and push to git
Commit messages should be brief since they are used to generate release notes.
Messages should say WHY the change was made and not WHAT was changed.

View File

@@ -2,6 +2,7 @@
mode: primary
hidden: true
model: opencode/claude-haiku-4-5
color: "#44BA81"
tools:
"*": false
"github-triage": true

View File

@@ -10,7 +10,17 @@
"options": {},
},
},
"mcp": {},
"permission": {
"bash": {
"ls foo": "ask",
},
},
"mcp": {
"context7": {
"type": "remote",
"url": "https://mcp.context7.com/mcp",
},
},
"tools": {
"github-triage": false,
},

View File

@@ -1,7 +1,4 @@
## Debugging
- To test opencode in the `packages/opencode` directory you can run `bun dev`
## Tool Calling
- To regenerate the javascript SDK, run ./packages/sdk/js/script/build.ts
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
- the default branch in this repo is `dev`

View File

@@ -14,10 +14,10 @@ However, any UI or core product feature must go through a design review with the
If you are unsure if a PR would be accepted, feel free to ask a maintainer or look for issues with any of the following labels:
- [`help wanted`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Ahelp-wanted)
- [`good first issue`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22)
- [`bug`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug)
- [`perf`](https://github.com/sst/opencode/issues?q=is%3Aopen%20is%3Aissue%20label%3A%22perf%22)
- [`help wanted`](https://github.com/anomalyco/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Ahelp-wanted)
- [`good first issue`](https://github.com/anomalyco/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22)
- [`bug`](https://github.com/anomalyco/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug)
- [`perf`](https://github.com/anomalyco/opencode/issues?q=is%3Aopen%20is%3Aissue%20label%3A%22perf%22)
> [!NOTE]
> PRs that ignore these guardrails will likely be closed.
@@ -34,6 +34,36 @@ Want to take on an issue? Leave a comment and a maintainer may assign it to you
bun dev
```
### Running against a different directory
By default, `bun dev` runs OpenCode in the `packages/opencode` directory. To run it against a different directory or repository:
```bash
bun dev <directory>
```
To run OpenCode in the root of the opencode repo itself:
```bash
bun dev .
```
### Building a "localcode"
To compile a standalone executable:
```bash
./packages/opencode/script/build.ts --single
```
Then run it with:
```bash
./packages/opencode/dist/opencode-<platform>/bin/opencode
```
Replace `<platform>` with your platform (e.g., `darwin-arm64`, `linux-x64`).
- Core pieces:
- `packages/opencode`: OpenCode core business logic & server.
- `packages/opencode/src/cli/cmd/tui/`: The TUI code, written in SolidJS with [opentui](https://github.com/sst/opentui)

View File

@@ -11,7 +11,7 @@
<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/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></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>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
@@ -30,8 +30,8 @@ scoop bucket add extras; scoop install extras/opencode # Windows
choco install opencode # Windows
brew install opencode # macOS and Linux
paru -S opencode-bin # Arch Linux
mise use -g github:sst/opencode # Any OS
nix run nixpkgs#opencode # or github:sst/opencode for latest dev branch
mise use -g opencode # Any OS
nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev branch
```
> [!TIP]
@@ -39,7 +39,7 @@ nix run nixpkgs#opencode # or github:sst/opencode for latest dev branc
### Desktop App (BETA)
OpenCode is also available as a desktop application. Download directly from the [releases page](https://github.com/sst/opencode/releases) or [opencode.ai/download](https://opencode.ai/download).
OpenCode is also available as a desktop application. Download directly from the [releases page](https://github.com/anomalyco/opencode/releases) or [opencode.ai/download](https://opencode.ai/download).
| Platform | Download |
| --------------------- | ------------------------------------- |

View File

@@ -11,7 +11,7 @@
<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/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></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>
[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
@@ -30,8 +30,8 @@ scoop bucket add extras; scoop install extras/opencode # Windows
choco install opencode # Windows
brew install opencode # macOS 與 Linux
paru -S opencode-bin # Arch Linux
mise use -g github:sst/opencode # 任何作業系統
nix run nixpkgs#opencode # 或使用 github:sst/opencode 以取得最新開發分支
mise use -g github:anomalyco/opencode # 任何作業系統
nix run nixpkgs#opencode # 或使用 github:anomalyco/opencode 以取得最新開發分支
```
> [!TIP]
@@ -39,7 +39,7 @@ nix run nixpkgs#opencode # 或使用 github:sst/opencode 以取得最
### 桌面應用程式 (BETA)
OpenCode 也提供桌面版應用程式。您可以直接從 [發佈頁面 (releases page)](https://github.com/sst/opencode/releases) 或 [opencode.ai/download](https://opencode.ai/download) 下載。
OpenCode 也提供桌面版應用程式。您可以直接從 [發佈頁面 (releases page)](https://github.com/anomalyco/opencode/releases) 或 [opencode.ai/download](https://opencode.ai/download) 下載。
| 平台 | 下載連結 |
| --------------------- | ------------------------------------- |

View File

@@ -182,3 +182,10 @@
| 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) |

144
bun.lock
View File

@@ -22,7 +22,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.0.203",
"version": "1.0.224",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -70,7 +70,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.0.203",
"version": "1.0.224",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -98,7 +98,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.0.203",
"version": "1.0.224",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -125,7 +125,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.0.203",
"version": "1.0.224",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -149,7 +149,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.0.203",
"version": "1.0.224",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -173,13 +173,14 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.0.203",
"version": "1.0.224",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@solid-primitives/storage": "catalog:",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-http": "~2",
"@tauri-apps/plugin-notification": "~2",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-os": "~2",
"@tauri-apps/plugin-process": "~2",
@@ -200,7 +201,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.0.203",
"version": "1.0.224",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -229,7 +230,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.0.203",
"version": "1.0.224",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -245,7 +246,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.0.203",
"version": "1.0.224",
"bin": {
"opencode": "./bin/opencode",
},
@@ -254,7 +255,7 @@
"@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.5.1",
"@ai-sdk/amazon-bedrock": "3.0.57",
"@ai-sdk/anthropic": "2.0.54",
"@ai-sdk/anthropic": "2.0.56",
"@ai-sdk/azure": "2.0.82",
"@ai-sdk/cerebras": "1.0.33",
"@ai-sdk/cohere": "2.0.21",
@@ -268,13 +269,14 @@
"@ai-sdk/openai-compatible": "1.0.29",
"@ai-sdk/perplexity": "2.0.22",
"@ai-sdk/provider": "2.0.0",
"@ai-sdk/provider-utils": "3.0.18",
"@ai-sdk/provider-utils": "3.0.19",
"@ai-sdk/togetherai": "1.0.30",
"@ai-sdk/vercel": "1.0.31",
"@ai-sdk/xai": "2.0.42",
"@clack/prompts": "1.0.0-alpha.1",
"@hono/standard-validator": "0.1.5",
"@hono/zod-validator": "catalog:",
"@modelcontextprotocol/sdk": "1.25.1",
"@modelcontextprotocol/sdk": "1.15.1",
"@octokit/graphql": "9.0.2",
"@octokit/rest": "catalog:",
"@openauthjs/openauth": "catalog:",
@@ -283,15 +285,16 @@
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "1.5.2",
"@opentui/core": "0.1.63",
"@opentui/solid": "0.1.63",
"@opentui/core": "0.1.67",
"@opentui/solid": "0.1.67",
"@parcel/watcher": "2.5.1",
"@pierre/diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
"@standard-schema/spec": "1.0.0",
"@zip.js/zip.js": "2.7.62",
"ai": "catalog:",
"bun-pty": "0.4.2",
"bonjour-service": "1.3.0",
"bun-pty": "0.4.4",
"chokidar": "4.0.3",
"clipboardy": "4.0.0",
"decimal.js": "10.5.0",
@@ -345,7 +348,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.0.203",
"version": "1.0.224",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -365,7 +368,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.0.203",
"version": "1.0.224",
"devDependencies": {
"@hey-api/openapi-ts": "0.88.1",
"@tsconfig/node22": "catalog:",
@@ -376,7 +379,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.0.203",
"version": "1.0.224",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -389,7 +392,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.0.203",
"version": "1.0.224",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -401,8 +404,10 @@
"@solidjs/meta": "catalog:",
"@typescript/native-preview": "catalog:",
"fuzzysort": "catalog:",
"katex": "0.16.27",
"luxon": "catalog:",
"marked": "catalog:",
"marked-katex-extension": "5.1.6",
"marked-shiki": "catalog:",
"remeda": "catalog:",
"shiki": "catalog:",
@@ -414,6 +419,7 @@
"@tailwindcss/vite": "catalog:",
"@tsconfig/node22": "catalog:",
"@types/bun": "catalog:",
"@types/katex": "0.16.7",
"@types/luxon": "catalog:",
"tailwindcss": "catalog:",
"typescript": "catalog:",
@@ -424,7 +430,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "1.0.203",
"version": "1.0.224",
"dependencies": {
"zod": "catalog:",
},
@@ -435,7 +441,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.0.203",
"version": "1.0.224",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -562,10 +568,12 @@
"@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
"@ai-sdk/togetherai": ["@ai-sdk/togetherai@1.0.30", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.29", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9bxQbIXnWSN4bNismrza3NvIo+ui/Y3pj3UN6e9vCszCWFCN45RgISi4oDe10RqmzaJ/X8cfO/Tem+K8MT3wGQ=="],
"@ai-sdk/vercel": ["@ai-sdk/vercel@1.0.31", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.30", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ggvwAMt/KsbqcdR6ILQrjwrRONLV/8aG6rOLbjcOGvV0Ai+WdZRRKQj5nOeQ06PvwVQtKdkp7S4IinpXIhCiHg=="],
"@ai-sdk/xai": ["@ai-sdk/xai@2.0.42", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.29", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wlwO4yRoZ/d+ca29vN8SDzxus7POdnL7GBTyRdSrt6icUF0hooLesauC8qRUC4aLxtqvMEc1YHtJOU7ZnLWbTQ=="],
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
@@ -900,8 +908,6 @@
"@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.88.1", "", { "dependencies": { "@hey-api/codegen-core": "^0.3.3", "@hey-api/json-schema-ref-parser": "1.2.2", "ansi-colors": "4.1.3", "c12": "3.3.2", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.2" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-x/nDTupOnV9VuSeNIiJpgIpc915GHduhyseJeMTnI0JMsXaObmpa0rgPr3ASVEYMLgpvqozIEG1RTOOnal6zLQ=="],
"@hono/node-server": ["@hono/node-server@1.19.7", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw=="],
"@hono/standard-validator": ["@hono/standard-validator@0.1.5", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-EIyZPPwkyLn6XKwFj5NBEWHXhXbgmnVh2ceIFo5GO7gKI9WmzTjPDKnppQB0KrqKeAkq3kpoW4SIbu5X1dgx3w=="],
"@hono/zod-validator": ["@hono/zod-validator@0.4.2", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-1rrlBg+EpDPhzOV4hT9pxr5+xDVmKuz6YJl+la7VCwK6ass5ldyKm5fD+umJdV2zhHD6jROoCCv8NbTwyfhT0g=="],
@@ -1080,11 +1086,13 @@
"@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="],
"@leichtgewicht/ip-codec": ["@leichtgewicht/ip-codec@2.0.5", "", {}, "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="],
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
"@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.1", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.15.1", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-W/XlN9c528yYn+9MQkVjxiTPgPxoxt+oczfjHBDsJx0+59+O7B75Zhsp0B16Xbwbz8ANISDajh6+V7nIcPMc5w=="],
"@motionone/animation": ["@motionone/animation@10.18.0", "", { "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw=="],
@@ -1188,21 +1196,21 @@
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@opentui/core": ["@opentui/core@0.1.63", "", { "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.63", "@opentui/core-darwin-x64": "0.1.63", "@opentui/core-linux-arm64": "0.1.63", "@opentui/core-linux-x64": "0.1.63", "@opentui/core-win32-arm64": "0.1.63", "@opentui/core-win32-x64": "0.1.63", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-m4xZQTNCnHXWUWCnGvacJ3Gts1H2aMwP5V/puAG77SDb51jm4W/QOyqAAdgeSakkb9II+8FfUpApX7sfwRXPUg=="],
"@opentui/core": ["@opentui/core@0.1.67", "", { "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.67", "@opentui/core-darwin-x64": "0.1.67", "@opentui/core-linux-arm64": "0.1.67", "@opentui/core-linux-x64": "0.1.67", "@opentui/core-win32-arm64": "0.1.67", "@opentui/core-win32-x64": "0.1.67", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-zmfyA10QUbzT6ohacPoHmGiYzuJrDSCfQWRWrKtao0BrHj9bii73qWy3V/eR4ibVueoRREwxJs5GlBOSvK6IoA=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.63", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jKCThZGiiublKkP/hMtDtl1MLCw5NU0hMNJdEYvz1WLT9bzliWf6Kb7MIDAmk32XlbQW8/RHdp+hGyGDXK62OQ=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.67", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LtOcTlFD+kO7neItmkiF77H8cnjTYzBOZe8JQGwRSt9aaCke3UzMvLxmQnj4BP/kPC3hi9V6NRnFdptz0sJZIQ=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.63", "", { "os": "darwin", "cpu": "x64" }, "sha512-rfNxynHzJpxN9i+SAMnn1NToEc8rYj64BsOxY78JNsm4Gg1Js1uyMaawwh2WbdGknFy4cDXS9QwkUMdMcfnjiw=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.67", "", { "os": "darwin", "cpu": "x64" }, "sha512-9i+awVWgpEVqZhFLaLq8usNGyCiyT5QxMLy6eH7JmRic79S34u23HfxiniGRtdYh3aqpm9SbLzo60v0nRIUkCA=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.63", "", { "os": "linux", "cpu": "arm64" }, "sha512-wG9d6mHWWKZGrzxYS4c+BrcEGXBv/MYBUPSyjP/lD0CxT+X3h6CYhI317JkRyMNfh3vI9CpAKGFTOFvrTTHimQ=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.67", "", { "os": "linux", "cpu": "arm64" }, "sha512-WLjnTM3Ig//SRo0FUZYZJ5TITVbR6dKDVg6axU2D+sMoUzJMBP/Xo04q/TvZ3wP764Yca9l7oVMKWDxHlygyjQ=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.63", "", { "os": "linux", "cpu": "x64" }, "sha512-TKSzFv4BgWW3RB/iZmq5qxTR4/tRaXo8IZNnVR+LFzShbPOqhUi466AByy9SUmCxD8uYjmMDFYfKtkCy0AnAwA=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.67", "", { "os": "linux", "cpu": "x64" }, "sha512-5UbZ/TqWi/DAmHIZL4NvhdpgTwglszRiddkRiQ8cT0IbnE4lutd4XxWUWcLKwsNT1YJv32TtcGWkuthluLiriQ=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.63", "", { "os": "win32", "cpu": "arm64" }, "sha512-CBWPyPognERP0Mq4eC1q01Ado2C2WU+BLTgMdhyt+E2P4w8rPhJ2kCt2MNxO66vQUiynspmZkgjQr0II/VjxWA=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.67", "", { "os": "win32", "cpu": "arm64" }, "sha512-KNam5rObhN8/U9+GVVuvtAlGXp3MfdMHnw4W2P6YH7xp8HTsLvABUT91SJEyJ/ktVe9e1itLDG2fDHSoA5NbUg=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.63", "", { "os": "win32", "cpu": "x64" }, "sha512-qEp6h//FrT+TQiiHm87wZWUwqTPTqIy1ZD+8R+VCUK+usoQiOAD2SqrYnM7W8JkCMGn5/TKm/GaKLyx/qlK4VA=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.67", "", { "os": "win32", "cpu": "x64" }, "sha512-740lkOw42zLNh9YfahXjCwV2DS/amH2uMDh3tCADDCLckrMhemIhqArXDiMlalDxDqYspoaZCpBsFVsG9dMS6A=="],
"@opentui/solid": ["@opentui/solid@0.1.63", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.63", "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-Gccln4qRucAoaoQEZ4NPAHvGmVYzU/8aKCLG8EPgwCKTcpUzlqYt4357cDHq4cnCNOcXOC06hTz/0pK9r0dqXA=="],
"@opentui/solid": ["@opentui/solid@0.1.67", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.67", "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-dVNq0+PJIdNb63D0T7vcbyVF/ZvLCihGvivTU50zDOzd0Sk5prbrIfpG8+DjMErFubXfdZQvdy/PqFdtw0rjtQ=="],
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
@@ -1702,6 +1710,8 @@
"@tauri-apps/plugin-http": ["@tauri-apps/plugin-http@2.5.4", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-/i4U/9za3mrytTgfRn5RHneKubZE/dwRmshYwyMvNRlkWjvu1m4Ma72kcbVJMZFGXpkbl+qLyWMGrihtWB76Zg=="],
"@tauri-apps/plugin-notification": ["@tauri-apps/plugin-notification@2.3.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg=="],
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew=="],
"@tauri-apps/plugin-os": ["@tauri-apps/plugin-os@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A=="],
@@ -1770,6 +1780,8 @@
"@types/jsonwebtoken": ["@types/jsonwebtoken@8.5.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg=="],
"@types/katex": ["@types/katex@0.16.7", "", {}, "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="],
"@types/luxon": ["@types/luxon@3.7.1", "", {}, "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg=="],
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
@@ -1886,9 +1898,7 @@
"ai": ["ai@5.0.97", "", { "dependencies": { "@ai-sdk/gateway": "2.0.12", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8zBx0b/owis4eJI2tAlV8a1Rv0BANmLxontcAelkLNwEHhgfgXeKpDkhNB6OgV+BJSwboIUDkgd9312DdJnCOQ=="],
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
@@ -2004,6 +2014,8 @@
"body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="],
"bonjour-service": ["bonjour-service@1.3.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA=="],
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
"bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="],
@@ -2032,7 +2044,7 @@
"bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="],
"bun-pty": ["bun-pty@0.4.2", "", {}, "sha512-sHImDz6pJDsHAroYpC9ouKVgOyqZ7FP3N+stX5IdMddHve3rf9LIZBDomQcXrACQ7sQDNuwZQHG8BKR7w8krkQ=="],
"bun-pty": ["bun-pty@0.4.4", "", {}, "sha512-WK4G6uWsZgu1v4hKIlw6G1q2AOf8Rbga2Yr7RnxArVjjyb+mtVa/CFc9GOJf+OYSJSH8k7LonAtQOVeNAddRyg=="],
"bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
@@ -2248,6 +2260,8 @@
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
"dns-packet": ["dns-packet@5.6.1", "", { "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" } }, "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw=="],
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
@@ -2388,7 +2402,7 @@
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fast-xml-parser": ["fast-xml-parser@4.4.1", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw=="],
@@ -2764,9 +2778,7 @@
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
@@ -2782,6 +2794,8 @@
"jwt-decode": ["jwt-decode@3.1.2", "", {}, "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="],
"katex": ["katex@0.16.27", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw=="],
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
@@ -2872,6 +2886,8 @@
"marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
"marked-katex-extension": ["marked-katex-extension@5.1.6", "", { "peerDependencies": { "katex": ">=0.16 <0.17", "marked": ">=4 <18" } }, "sha512-vYpLXwmlIDKILIhJtiRTgdyZRn5sEYdFBuTmbpjD7lbCIzg0/DWyK3HXIntN3Tp8zV6hvOUgpZNLWRCgWVc24A=="],
"marked-shiki": ["marked-shiki@1.2.1", "", { "peerDependencies": { "marked": ">=7.0.0", "shiki": ">=1.0.0" } }, "sha512-yHxYQhPY5oYaIRnROn98foKhuClark7M373/VpLxiy5TrDu9Jd/LsMwo8w+U91Up4oDb9IXFrP0N1MFRz8W/DQ=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
@@ -3026,6 +3042,8 @@
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"multicast-dns": ["multicast-dns@7.2.5", "", { "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" }, "bin": { "multicast-dns": "cli.js" } }, "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg=="],
"mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="],
"mysql2": ["mysql2@3.14.4", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-Cs/jx3WZPNrYHVz+Iunp9ziahaG5uFMvD2R8Zlmc194AqXNxt9HBNu7ZsPYrUtmJsF0egETCWIdMIYAwOGjL1w=="],
@@ -3362,8 +3380,6 @@
"remeda": ["remeda@2.26.0", "", { "dependencies": { "type-fest": "^4.41.0" } }, "sha512-lmNNwtaC6Co4m0WTTNoZ/JlpjEqAjPZO0+czC9YVRQUpkbS4x8Hmh+Mn9HPfJfiXqUQ5IXXgSXSOB2pBKAytdA=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"reselect": ["reselect@4.1.8", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="],
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
@@ -3600,6 +3616,8 @@
"three": ["three@0.177.0", "", {}, "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg=="],
"thunky": ["thunky@1.1.0", "", {}, "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="],
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
@@ -3736,6 +3754,8 @@
"update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"url": ["url@0.10.3", "", { "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" } }, "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ=="],
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
@@ -3888,40 +3908,26 @@
"@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
"@ai-sdk/cerebras/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
"@ai-sdk/cohere/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
"@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
"@ai-sdk/deepinfra/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
"@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
"@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.50", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-21PaHfoLmouOXXNINTsZJsMw+wE5oLR2He/1kq/sKokTVKyq7ObGT1LDk6ahwxaz/GoaNaGankMh+EgVcdv2Cw=="],
"@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@2.0.44", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-c5dck36FjqiVoeeMJQLTEmUheoURcGTU/nBT6iJu8/nZiKFT/y8pD85KMDRB7RerRYaaQOtslR2d6/5PditiRw=="],
"@ai-sdk/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
"@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
"@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="],
"@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
"@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
"@ai-sdk/perplexity/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
"@ai-sdk/togetherai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
"@ai-sdk/togetherai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
"@ai-sdk/vercel/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.30", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-thubwhRtv9uicAxSWwNpinM7hiL/0CkhL/ymPaHuKvI494J7HIzn8KQZQ2ymRz284WTIZnI7VMyyejxW4RMM6w=="],
"@ai-sdk/vercel/@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="],
"@ai-sdk/vercel/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
"@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
"@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
"@astrojs/cloudflare/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
"@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="],
@@ -4048,11 +4054,9 @@
"@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
"@modelcontextprotocol/sdk/jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
"@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
"@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
"@modelcontextprotocol/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@octokit/auth-app/@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="],
@@ -4278,6 +4282,8 @@
"jsonwebtoken/jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="],
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
"lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
@@ -4302,7 +4308,7 @@
"nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.54", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-6OSFMkt5NkAchH7o0W+dI2h6yR8EPXx7Yl6txyh0gadLlkf1UU/ScyoYlkxAW8UtGju/+apvwVTdLYEQuIsVVQ=="],
"opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XHJKu0Yvfu9SPzRfsAFESa+9T7f2YJY6TxykKMfRsAwpeWAiX/Gbx5J5uM15AzYC3Rw8tVP3oH+j7jEivENirQ=="],
"opencode/@ai-sdk/openai": ["@ai-sdk/openai@2.0.71", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tg+gj+R0z/On9P4V7hy7/7o04cQPjKGayMCL3gzWD/aNGjAKkhEnaocuNDidSnghizt8g2zJn16cAuAolnW+qQ=="],
@@ -4394,6 +4400,8 @@
"unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
"uri-js/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"utif2/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
"vitest/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
@@ -4908,8 +4916,6 @@
"opencode/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
"opencode/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
"opencontrol/@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
"opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="],

View File

@@ -1,2 +1,6 @@
[install]
exact = true
[test]
root = "./do-not-run-tests-from-root"

6
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1766870016,
"narHash": "sha256-fHmxAesa6XNqnIkcS6+nIHuEmgd/iZSP/VXxweiEuQw=",
"lastModified": 1767273430,
"narHash": "sha256-kDpoFwQ8GLrPiS3KL+sAwreXrph2KhdXuJzo5+vSLoo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5c2bc52fb9f8c264ed6c93bd20afa2ff5e763dce",
"rev": "76eec3925eb9bbe193934987d3285473dbcfad50",
"type": "github"
},
"original": {

View File

@@ -17,7 +17,7 @@
"aarch64-darwin"
"x86_64-darwin"
];
lib = nixpkgs.lib;
inherit (nixpkgs) lib;
forEachSystem = lib.genAttrs systems;
pkgsFor = system: nixpkgs.legacyPackages.${system};
packageJson = builtins.fromJSON (builtins.readFile ./packages/opencode/package.json);
@@ -70,12 +70,12 @@
in
{
default = mkPackage {
version = packageJson.version;
inherit (packageJson) version;
src = ./.;
scripts = ./nix/scripts;
target = bunTarget.${system};
modelsDev = "${modelsDev.${system}}/dist/_api.json";
mkNodeModules = mkNodeModules;
inherit mkNodeModules;
};
}
);

View File

@@ -87,7 +87,7 @@ This will walk you through installing the GitHub app, creating the workflow, and
fetch-depth: 1
- name: Run opencode
uses: sst/opencode/github@latest
uses: anomalyco/opencode/github@latest
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
with:
@@ -98,7 +98,7 @@ This will walk you through installing the GitHub app, creating the workflow, and
## Support
This is an early release. If you encounter issues or have feedback, please create an issue at https://github.com/sst/opencode/issues.
This is an early release. If you encounter issues or have feedback, please create an issue at https://github.com/anomalyco/opencode/issues.
## Development

View File

@@ -41,7 +41,7 @@ runs:
id: version
shell: bash
run: |
VERSION=$(curl -sf https://api.github.com/repos/sst/opencode/releases/latest | grep -o '"tag_name": *"[^"]*"' | cut -d'"' -f4)
VERSION=$(curl -sf https://api.github.com/repos/anomalyco/opencode/releases/latest | grep -o '"tag_name": *"[^"]*"' | cut -d'"' -f4)
echo "version=${VERSION:-latest}" >> $GITHUB_OUTPUT
- name: Cache opencode

View File

@@ -281,7 +281,7 @@ async function assertOpencodeConnected() {
connected = true
break
} catch (e) {}
await new Promise((resolve) => setTimeout(resolve, 300))
await Bun.sleep(300)
} while (retry++ < 30)
if (!connected) {

10
install
View File

@@ -147,8 +147,8 @@ INSTALL_DIR=$HOME/.opencode/bin
mkdir -p "$INSTALL_DIR"
if [ -z "$requested_version" ]; then
url="https://github.com/sst/opencode/releases/latest/download/$filename"
specific_version=$(curl -s https://api.github.com/repos/sst/opencode/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
url="https://github.com/anomalyco/opencode/releases/latest/download/$filename"
specific_version=$(curl -s https://api.github.com/repos/anomalyco/opencode/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
if [[ $? -ne 0 || -z "$specific_version" ]]; then
echo -e "${RED}Failed to fetch version information${NC}"
@@ -157,14 +157,14 @@ if [ -z "$requested_version" ]; then
else
# Strip leading 'v' if present
requested_version="${requested_version#v}"
url="https://github.com/sst/opencode/releases/download/v${requested_version}/$filename"
url="https://github.com/anomalyco/opencode/releases/download/v${requested_version}/$filename"
specific_version=$requested_version
# Verify the release exists before downloading
http_status=$(curl -sI -o /dev/null -w "%{http_code}" "https://github.com/sst/opencode/releases/tag/v${requested_version}")
http_status=$(curl -sI -o /dev/null -w "%{http_code}" "https://github.com/anomalyco/opencode/releases/tag/v${requested_version}")
if [ "$http_status" = "404" ]; then
echo -e "${RED}Error: Release v${requested_version} not found${NC}"
echo -e "${MUTED}Available releases: https://github.com/sst/opencode/releases${NC}"
echo -e "${MUTED}Available releases: https://github.com/anomalyco/opencode/releases${NC}"
exit 1
fi
fi

View File

@@ -1,3 +1,3 @@
{
"nodeModules": "sha256-d10pu1tpwBbZ4YZqG0WcaK6W1+K2+Q0wdPjuI1rVIpY="
"nodeModules": "sha256-uJDhOieOdMQLORyuOWtgtjLoMnNEQPrDcyij9TX0aTw="
}

View File

@@ -1,18 +1,26 @@
{ hash, lib, stdenvNoCC, bun, cacert, curl }:
{
hash,
lib,
stdenvNoCC,
bun,
cacert,
curl,
}:
args:
stdenvNoCC.mkDerivation {
pname = "opencode-node_modules";
version = args.version;
src = args.src;
inherit (args) version src;
impureEnvVars =
lib.fetchers.proxyImpureEnvVars
++ [
"GIT_PROXY_COMMAND"
"SOCKS_SERVER"
];
impureEnvVars = lib.fetchers.proxyImpureEnvVars ++ [
"GIT_PROXY_COMMAND"
"SOCKS_SERVER"
];
nativeBuildInputs = [ bun cacert curl ];
nativeBuildInputs = [
bun
cacert
curl
];
dontConfigure = true;

View File

@@ -1,7 +1,13 @@
{ lib, stdenvNoCC, bun, ripgrep, makeBinaryWrapper }:
{
lib,
stdenvNoCC,
bun,
ripgrep,
makeBinaryWrapper,
}:
args:
let
scripts = args.scripts;
inherit (args) scripts;
mkModules =
attrs:
args.mkNodeModules (
@@ -14,13 +20,10 @@ let
in
stdenvNoCC.mkDerivation (finalAttrs: {
pname = "opencode";
version = args.version;
src = args.src;
inherit (args) version src;
node_modules = mkModules {
version = finalAttrs.version;
src = finalAttrs.src;
inherit (finalAttrs) version src;
};
nativeBuildInputs = [
@@ -122,7 +125,7 @@ stdenvNoCC.mkDerivation (finalAttrs: {
It combines a TypeScript/JavaScript core with a Go-based TUI
to provide an interactive AI coding experience.
'';
homepage = "https://github.com/sst/opencode";
homepage = "https://github.com/anomalyco/opencode";
license = lib.licenses.mit;
platforms = [
"aarch64-linux"

View File

@@ -31,9 +31,13 @@ for (const [name, wasmPath] of byName) {
next = next.replaceAll("tree-sitter.wasm", mainWasm).replaceAll("web-tree-sitter/tree-sitter.wasm", mainWasm)
// Collapse any relative prefixes before absolute store paths (e.g., "../../../..//nix/store/...")
const nixStorePrefix = process.env.NIX_STORE || "/nix/store"
next = next.replace(/(\.\/)+/g, "./")
next = next.replace(/(\.\.\/)+\/?(\/nix\/store[^"']+)/g, "/$2")
next = next.replace(/(["'])\/{2,}(\/nix\/store[^"']+)(["'])/g, "$1/$2$3")
next = next.replace(/(["'])\/\/(nix\/store[^"']+)(["'])/g, "$1/$2$3")
next = next.replace(
new RegExp(`(\\.\\.\\/)+\\/{1,2}(${nixStorePrefix.replace(/^\//, "").replace(/\//g, "\\/")}[^"']+)`, "g"),
"/$2",
)
next = next.replace(new RegExp(`(["'])\\/{2,}(\\/${nixStorePrefix.replace(/\//g, "\\/")}[^"']+)(["'])`, "g"), "$1$2$3")
next = next.replace(new RegExp(`(["'])\\/\\/(${nixStorePrefix.replace(/\//g, "\\/")}[^"']+)(["'])`, "g"), "$1$2$3")
if (next !== content) fs.writeFileSync(file, next)

View File

@@ -10,7 +10,8 @@
"typecheck": "bun turbo typecheck",
"prepare": "husky",
"random": "echo 'Random script'",
"hello": "echo 'Hello World!'"
"hello": "echo 'Hello World!'",
"test": "echo 'do not run tests from root' && exit 1"
},
"workspaces": {
"packages": [
@@ -75,7 +76,7 @@
},
"repository": {
"type": "git",
"url": "https://github.com/sst/opencode"
"url": "https://github.com/anomalyco/opencode"
},
"license": "MIT",
"prettier": {

View File

@@ -1,28 +1,13 @@
# Agent Guidelines for @opencode/app
## Debugging
## Build/Test Commands
- To test the opencode app, use the playwrite mcp server, the app is already
running at http://localhost:3000
- NEVER try to restart the app, or the server process, EVER.
- **Development**: `bun run dev` (starts Vite dev server on port 3000)
- **Build**: `bun run build` (production build)
- **Preview**: `bun run serve` (preview production build)
- **Validation**: Use `bun run typecheck` only - do not build or run project for validation
- **Testing**: Do not create or run automated tests
## SolidJS
## Code Style
- Always prefer `createStore` over multiple `createSignal` calls
- **Framework**: SolidJS with TypeScript
- **Imports**: Use `@/` alias for src/ directory (e.g., `import Button from "@/ui/button"`)
- **Formatting**: Prettier configured with semicolons disabled, 120 character line width
- **Components**: Use function declarations, splitProps for component props
- **Types**: Define interfaces for component props, avoid `any` type
- **CSS**: TailwindCSS with custom CSS variables theme system
- **Naming**: PascalCase for components, camelCase for variables/functions, snake_case for file names
- **File Structure**: UI primitives in `/ui/`, higher-level components in `/components/`, pages in `/pages/`, providers in `/providers/`
## Tool Calling
## Key Dependencies
- SolidJS, @solidjs/router, @kobalte/core (UI primitives)
- TailwindCSS 4.x with @tailwindcss/vite
- Custom theme system with CSS variables
No special rules files found.
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.

View File

@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="en" style="background-color: var(--background-base)">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -13,14 +13,39 @@
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
<meta property="og:image" content="/social-share.png" />
<meta property="twitter:image" content="/social-share.png" />
</head>
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
<script>
<!-- Theme preload script - applies cached theme to avoid FOUC -->
<script id="oc-theme-preload-script">
;(function () {
const savedTheme = localStorage.getItem("theme") || "oc-1"
document.documentElement.setAttribute("data-theme", savedTheme)
var themeId = localStorage.getItem("opencode-theme-id")
if (!themeId) return
var scheme = localStorage.getItem("opencode-color-scheme") || "system"
var isDark = scheme === "dark" || (scheme === "system" && matchMedia("(prefers-color-scheme: dark)").matches)
var mode = isDark ? "dark" : "light"
document.documentElement.dataset.theme = themeId
document.documentElement.dataset.colorScheme = mode
if (themeId === "oc-1") return
var css = localStorage.getItem("opencode-theme-css-" + themeId + "-" + mode)
if (css) {
var style = document.createElement("style")
style.id = "oc-theme-preload"
style.textContent =
":root{color-scheme:" +
mode +
";--text-mix-blend-mode:" +
(isDark ? "plus-lighter" : "multiply") +
";" +
css +
"}"
document.head.appendChild(style)
}
})()
</script>
</head>
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root" class="flex flex-col h-screen"></div>
<script src="/src/entry.tsx" type="module"></script>

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.0.203",
"version": "1.0.224",
"description": "",
"type": "module",
"exports": {

View File

@@ -1,5 +1,5 @@
import "@/index.css"
import { ErrorBoundary, Show } from "solid-js"
import { ErrorBoundary, Show, type ParentProps } from "solid-js"
import { Router, Route, Navigate } from "@solidjs/router"
import { MetaProvider } from "@solidjs/meta"
import { Font } from "@opencode-ai/ui/font"
@@ -8,11 +8,15 @@ import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
import { CodeComponentProvider } from "@opencode-ai/ui/context/code"
import { Diff } from "@opencode-ai/ui/diff"
import { Code } from "@opencode-ai/ui/code"
import { ThemeProvider } from "@opencode-ai/ui/theme"
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 { TerminalProvider } from "@/context/terminal"
import { PromptProvider } from "@/context/prompt"
import { FileProvider } from "@/context/file"
import { NotificationProvider } from "@/context/notification"
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
import { CommandProvider } from "@/context/command"
@@ -29,7 +33,7 @@ declare global {
}
}
const url = iife(() => {
const defaultServerUrl = iife(() => {
const param = new URLSearchParams(document.location.search).get("url")
if (param) return param
@@ -41,52 +45,71 @@ const url = iife(() => {
return window.location.origin
})
function ServerKey(props: ParentProps) {
const server = useServer()
return (
<Show when={server.url} keyed>
{props.children}
</Show>
)
}
export function App() {
return (
<MetaProvider>
<Font />
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
<DialogProvider>
<MarkedProvider>
<DiffComponentProvider component={Diff}>
<CodeComponentProvider component={Code}>
<GlobalSDKProvider url={url}>
<GlobalSyncProvider>
<LayoutProvider>
<NotificationProvider>
<Router
root={(props) => (
<CommandProvider>
<Layout>{props.children}</Layout>
</CommandProvider>
)}
>
<Route path="/" component={Home} />
<Route path="/:dir" component={DirectoryLayout}>
<Route path="/" component={() => <Navigate href="session" />} />
<Route
path="/session/:id?"
component={(p) => (
<Show when={p.params.id || true} keyed>
<TerminalProvider>
<PromptProvider>
<Session />
</PromptProvider>
</TerminalProvider>
</Show>
)}
/>
</Route>
</Router>
</NotificationProvider>
</LayoutProvider>
</GlobalSyncProvider>
</GlobalSDKProvider>
</CodeComponentProvider>
</DiffComponentProvider>
</MarkedProvider>
</DialogProvider>
</ErrorBoundary>
<ThemeProvider>
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
<DialogProvider>
<MarkedProvider>
<DiffComponentProvider component={Diff}>
<CodeComponentProvider component={Code}>
<ServerProvider defaultUrl={defaultServerUrl}>
<ServerKey>
<GlobalSDKProvider>
<GlobalSyncProvider>
<Router
root={(props) => (
<PermissionProvider>
<LayoutProvider>
<NotificationProvider>
<CommandProvider>
<Layout>{props.children}</Layout>
</CommandProvider>
</NotificationProvider>
</LayoutProvider>
</PermissionProvider>
)}
>
<Route path="/" component={Home} />
<Route path="/:dir" component={DirectoryLayout}>
<Route path="/" component={() => <Navigate href="session" />} />
<Route
path="/session/:id?"
component={(p) => (
<Show when={p.params.id ?? "new"} keyed>
<TerminalProvider>
<FileProvider>
<PromptProvider>
<Session />
</PromptProvider>
</FileProvider>
</TerminalProvider>
</Show>
)}
/>
</Route>
</Router>
</GlobalSyncProvider>
</GlobalSDKProvider>
</ServerKey>
</ServerProvider>
</CodeComponentProvider>
</DiffComponentProvider>
</MarkedProvider>
</DialogProvider>
</ErrorBoundary>
</ThemeProvider>
</MetaProvider>
)
}

View File

@@ -0,0 +1,180 @@
import { Button } from "@opencode-ai/ui/button"
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 { createStore } from "solid-js/store"
import { useGlobalSDK } from "@/context/global-sdk"
import { type LocalProject, getAvatarColors } from "@/context/layout"
import { Avatar } from "@opencode-ai/ui/avatar"
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
function getFilename(input: string) {
const parts = input.split("/")
return parts[parts.length - 1] || input
}
export function DialogEditProject(props: { project: LocalProject }) {
const dialog = useDialog()
const globalSDK = useGlobalSDK()
const folderName = createMemo(() => getFilename(props.project.worktree))
const defaultName = createMemo(() => props.project.name || folderName())
const [store, setStore] = createStore({
name: defaultName(),
color: props.project.icon?.color || "pink",
iconUrl: props.project.icon?.url || "",
saving: false,
})
const [dragOver, setDragOver] = 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)
reader.readAsDataURL(file)
}
function handleDrop(e: DragEvent) {
e.preventDefault()
setDragOver(false)
const file = e.dataTransfer?.files[0]
if (file) handleFileSelect(file)
}
function handleDragOver(e: DragEvent) {
e.preventDefault()
setDragOver(true)
}
function handleDragLeave() {
setDragOver(false)
}
function handleInputChange(e: Event) {
const input = e.target as HTMLInputElement
const file = input.files?.[0]
if (file) handleFileSelect(file)
}
function clearIcon() {
setStore("iconUrl", "")
}
async function handleSubmit(e: SubmitEvent) {
e.preventDefault()
if (!props.project.id) return
setStore("saving", true)
const name = store.name.trim() === folderName() ? "" : store.name.trim()
await globalSDK.client.project.update({
projectID: props.project.id,
name,
icon: { color: store.color, url: store.iconUrl },
})
setStore("saving", false)
dialog.close()
}
return (
<Dialog title="Edit project">
<form onSubmit={handleSubmit} class="flex flex-col gap-6 px-2.5 pb-3">
<div class="flex flex-col gap-4">
<TextField
autofocus
type="text"
label="Name"
placeholder={folderName()}
value={store.name}
onChange={(v) => setStore("name", v)}
/>
<div class="flex flex-col gap-2">
<label class="text-12-medium text-text-weak">Icon</label>
<div class="flex gap-3 items-start">
<div class="relative">
<div
class="size-16 rounded-lg overflow-hidden border border-dashed transition-colors cursor-pointer"
classList={{
"border-text-interactive-base bg-surface-info-base/20": dragOver(),
"border-border-base hover:border-border-strong": !dragOver(),
}}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onClick={() => document.getElementById("icon-upload")?.click()}
>
<Show
when={store.iconUrl}
fallback={
<div class="size-full flex items-center justify-center">
<Avatar
fallback={store.name || defaultName()}
{...getAvatarColors(store.color)}
class="size-full"
/>
</div>
}
>
<img src={store.iconUrl} alt="Project icon" class="size-full object-cover" />
</Show>
</div>
<Show when={store.iconUrl}>
<button
type="button"
class="absolute -top-1.5 -right-1.5 size-5 rounded-full bg-surface-raised-base border border-border-base flex items-center justify-center hover:bg-surface-raised-base-hover"
onClick={clearIcon}
>
<Icon name="close" class="size-3 text-icon-base" />
</button>
</Show>
</div>
<input id="icon-upload" type="file" accept="image/*" class="hidden" onChange={handleInputChange} />
<div class="flex flex-col gap-1.5 text-12-regular text-text-weak">
<span>Click or drag an image</span>
<span>Recommended: 128x128px</span>
</div>
</div>
</div>
<Show when={!store.iconUrl}>
<div class="flex flex-col gap-2">
<label class="text-12-medium text-text-weak">Color</label>
<div class="flex gap-2">
<For each={AVATAR_COLOR_KEYS}>
{(color) => (
<button
type="button"
class="relative size-8 rounded-md transition-all"
classList={{
"ring-2 ring-offset-2 ring-offset-surface-base ring-text-interactive-base":
store.color === color,
}}
style={{ background: getAvatarColors(color).background }}
onClick={() => setStore("color", color)}
>
<Avatar fallback={store.name || defaultName()} {...getAvatarColors(color)} class="size-full" />
</button>
)}
</For>
</div>
</div>
</Show>
</div>
<div class="flex justify-end gap-2">
<Button type="button" variant="ghost" size="large" onClick={() => dialog.close()}>
Cancel
</Button>
<Button type="submit" variant="primary" size="large" disabled={store.saving}>
{store.saving ? "Saving..." : "Save"}
</Button>
</div>
</form>
</Dialog>
)
}

View File

@@ -0,0 +1,114 @@
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { List } from "@opencode-ai/ui/list"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { createMemo } from "solid-js"
import { useGlobalSDK } from "@/context/global-sdk"
import { useGlobalSync } from "@/context/global-sync"
interface DialogSelectDirectoryProps {
title?: string
multiple?: boolean
onSelect: (result: string | string[] | null) => void
}
export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
const sync = useGlobalSync()
const sdk = useGlobalSDK()
const dialog = useDialog()
const home = createMemo(() => sync.data.path.home)
const root = createMemo(() => sync.data.path.home || sync.data.path.directory)
function join(base: string | undefined, rel: string) {
const b = (base ?? "").replace(/[\\/]+$/, "")
const r = rel.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")
if (!b) return r
if (!r) return b
return b + "/" + r
}
function display(rel: string) {
const full = join(root(), rel)
const h = home()
if (!h) return full
if (full === h) return "~"
if (full.startsWith(h + "/") || full.startsWith(h + "\\")) {
return "~" + full.slice(h.length)
}
return full
}
function normalizeQuery(query: string) {
const h = home()
if (!query) return query
if (query.startsWith("~/")) return query.slice(2)
if (h) {
const lc = query.toLowerCase()
const hc = h.toLowerCase()
if (lc === hc || lc.startsWith(hc + "/") || lc.startsWith(hc + "\\")) {
return query.slice(h.length).replace(/^[\\/]+/, "")
}
}
return query
}
async function fetchDirs(query: string) {
const directory = root()
if (!directory) return [] as string[]
const results = await sdk.client.find
.files({ directory, query, type: "directory", limit: 50 })
.then((x) => x.data ?? [])
.catch(() => [])
return results.map((x) => x.replace(/[\\/]+$/, ""))
}
const directories = async (filter: string) => {
const query = normalizeQuery(filter.trim())
return fetchDirs(query)
}
function resolve(rel: string) {
const absolute = join(root(), rel)
props.onSelect(props.multiple ? [absolute] : absolute)
dialog.close()
}
return (
<Dialog title={props.title ?? "Open project"}>
<List
search={{ placeholder: "Search folders", autofocus: true }}
emptyMessage="No folders found"
items={directories}
key={(x) => x}
onSelect={(path) => {
if (!path) return
resolve(path)
}}
>
{(rel) => {
const path = display(rel)
return (
<div class="w-full flex items-center justify-between rounded-md">
<div class="flex items-center gap-x-3 grow min-w-0">
<FileIcon node={{ path: rel, type: "directory" }} class="shrink-0 size-4" />
<div class="flex items-center text-14-regular min-w-0">
<span class="text-text-weak whitespace-nowrap overflow-hidden overflow-ellipsis truncate min-w-0">
{getDirectory(path)}
</span>
<span class="text-text-strong whitespace-nowrap">{getFilename(path)}</span>
</div>
</div>
</div>
)
}}
</List>
</Dialog>
)
}

View File

@@ -6,11 +6,11 @@ import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { useParams } from "@solidjs/router"
import { createMemo } from "solid-js"
import { useLayout } from "@/context/layout"
import { useLocal } from "@/context/local"
import { useFile } from "@/context/file"
export function DialogSelectFile() {
const layout = useLayout()
const local = useLocal()
const file = useFile()
const dialog = useDialog()
const params = useParams()
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
@@ -20,11 +20,13 @@ export function DialogSelectFile() {
<List
search={{ placeholder: "Search files", autofocus: true }}
emptyMessage="No files found"
items={local.file.searchFiles}
items={file.searchFiles}
key={(x) => x}
onSelect={(path) => {
if (path) {
tabs().open("file://" + path)
const value = file.tab(path)
tabs().open(value)
file.load(path)
}
dialog.close()
}}

View File

@@ -1,4 +1,5 @@
import { Component, createMemo, Show } from "solid-js"
import { Popover as Kobalte } from "@kobalte/core/popover"
import { Component, createMemo, createSignal, JSX, Show } from "solid-js"
import { useLocal } from "@/context/local"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { popularProviders } from "@/hooks/use-providers"
@@ -9,9 +10,12 @@ import { List } from "@opencode-ai/ui/list"
import { DialogSelectProvider } from "./dialog-select-provider"
import { DialogManageModels } from "./dialog-manage-models"
export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
const ModelList: Component<{
provider?: string
class?: string
onSelect: () => void
}> = (props) => {
const local = useLocal()
const dialog = useDialog()
const models = createMemo(() =>
local.model
@@ -20,6 +24,70 @@ export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
.filter((m) => (props.provider ? m.provider.id === props.provider : true)),
)
return (
<List
class={`flex-1 min-h-0 [&_[data-slot=list-scroll]]:flex-1 [&_[data-slot=list-scroll]]:min-h-0 ${props.class ?? ""}`}
search={{ placeholder: "Search models", autofocus: true }}
emptyMessage="No model results"
key={(x) => `${x.provider.id}:${x.id}`}
items={models}
current={local.model.current()}
filterKeys={["provider.name", "name", "id"]}
sortBy={(a, b) => a.name.localeCompare(b.name)}
groupBy={(x) => x.provider.name}
sortGroupsBy={(a, b) => {
if (a.category === "Recent" && b.category !== "Recent") return -1
if (b.category === "Recent" && a.category !== "Recent") return 1
const aProvider = a.items[0].provider.id
const bProvider = b.items[0].provider.id
if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
}}
onSelect={(x) => {
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
recent: true,
})
props.onSelect()
}}
>
{(i) => (
<div class="w-full flex items-center gap-x-2 text-13-regular">
<span class="truncate">{i.name}</span>
<Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}>
<Tag>Free</Tag>
</Show>
<Show when={i.latest}>
<Tag>Latest</Tag>
</Show>
</div>
)}
</List>
)
}
export const ModelSelectorPopover: Component<{
provider?: string
children: JSX.Element
}> = (props) => {
const [open, setOpen] = createSignal(false)
return (
<Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={8}>
<Kobalte.Trigger as="div">{props.children}</Kobalte.Trigger>
<Kobalte.Portal>
<Kobalte.Content 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">
<Kobalte.Title class="sr-only">Select model</Kobalte.Title>
<ModelList provider={props.provider} onSelect={() => setOpen(false)} class="p-1" />
</Kobalte.Content>
</Kobalte.Portal>
</Kobalte>
)
}
export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
const dialog = useDialog()
return (
<Dialog
title="Select model"
@@ -34,43 +102,7 @@ export const DialogSelectModel: Component<{ provider?: string }> = (props) => {
</Button>
}
>
<List
search={{ placeholder: "Search models", autofocus: true }}
emptyMessage="No model results"
key={(x) => `${x.provider.id}:${x.id}`}
items={models}
current={local.model.current()}
filterKeys={["provider.name", "name", "id"]}
sortBy={(a, b) => a.name.localeCompare(b.name)}
groupBy={(x) => x.provider.name}
sortGroupsBy={(a, b) => {
if (a.category === "Recent" && b.category !== "Recent") return -1
if (b.category === "Recent" && a.category !== "Recent") return 1
const aProvider = a.items[0].provider.id
const bProvider = b.items[0].provider.id
if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
if (!popularProviders.includes(aProvider) && popularProviders.includes(bProvider)) return 1
return popularProviders.indexOf(aProvider) - popularProviders.indexOf(bProvider)
}}
onSelect={(x) => {
local.model.set(x ? { modelID: x.id, providerID: x.provider.id } : undefined, {
recent: true,
})
dialog.close()
}}
>
{(i) => (
<div class="w-full flex items-center gap-x-3">
<span>{i.name}</span>
<Show when={i.provider.id === "opencode" && (!i.cost || i.cost?.input === 0)}>
<Tag>Free</Tag>
</Show>
<Show when={i.latest}>
<Tag>Latest</Tag>
</Show>
</div>
)}
</List>
<ModelList provider={props.provider} onSelect={() => dialog.close()} />
<Button
variant="ghost"
class="ml-3 mt-5 mb-6 text-text-base self-start"

View File

@@ -0,0 +1,179 @@
import { createEffect, createMemo, onCleanup } from "solid-js"
import { createStore, reconcile } from "solid-js/store"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
import { TextField } from "@opencode-ai/ui/text-field"
import { Button } from "@opencode-ai/ui/button"
import { normalizeServerUrl, serverDisplayName, useServer } from "@/context/server"
import { usePlatform } from "@/context/platform"
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
import { useNavigate } from "@solidjs/router"
type ServerStatus = { healthy: boolean; version?: string }
async function checkHealth(url: string, fetch?: typeof globalThis.fetch): Promise<ServerStatus> {
const sdk = createOpencodeClient({
baseUrl: url,
fetch,
signal: AbortSignal.timeout(3000),
})
return sdk.global
.health()
.then((x) => ({ healthy: x.data?.healthy === true, version: x.data?.version }))
.catch(() => ({ healthy: false }))
}
export function DialogSelectServer() {
const navigate = useNavigate()
const dialog = useDialog()
const server = useServer()
const platform = usePlatform()
const [store, setStore] = createStore({
url: "",
adding: false,
error: "",
status: {} as Record<string, ServerStatus | undefined>,
})
const items = createMemo(() => {
const current = server.url
const list = server.list
if (!current) return list
if (!list.includes(current)) return [current, ...list]
return [current, ...list.filter((x) => x !== current)]
})
const current = createMemo(() => items().find((x) => x === server.url) ?? items()[0])
const sortedItems = createMemo(() => {
const list = items()
if (!list.length) return list
const active = current()
const order = new Map(list.map((url, index) => [url, index] as const))
const rank = (value?: ServerStatus) => {
if (value?.healthy === true) return 0
if (value?.healthy === false) return 2
return 1
}
return list.slice().sort((a, b) => {
if (a === active) return -1
if (b === active) return 1
const diff = rank(store.status[a]) - rank(store.status[b])
if (diff !== 0) return diff
return (order.get(a) ?? 0) - (order.get(b) ?? 0)
})
})
async function refreshHealth() {
const results: Record<string, ServerStatus> = {}
await Promise.all(
items().map(async (url) => {
results[url] = await checkHealth(url, platform.fetch)
}),
)
setStore("status", reconcile(results))
}
createEffect(() => {
items()
refreshHealth()
const interval = setInterval(refreshHealth, 10_000)
onCleanup(() => clearInterval(interval))
})
function select(value: string, persist?: boolean) {
if (!persist && store.status[value]?.healthy === false) return
dialog.close()
if (persist) {
server.add(value)
navigate("/")
return
}
server.setActive(value)
navigate("/")
}
async function handleSubmit(e: SubmitEvent) {
e.preventDefault()
const value = normalizeServerUrl(store.url)
if (!value) return
setStore("adding", true)
setStore("error", "")
const result = await checkHealth(value, platform.fetch)
setStore("adding", false)
if (!result.healthy) {
setStore("error", "Could not connect to server")
return
}
setStore("url", "")
select(value, true)
}
return (
<Dialog title="Servers" description="Switch which OpenCode server this app connects to.">
<div class="flex flex-col gap-4 pb-4">
<List
search={{ placeholder: "Search servers", autofocus: true }}
emptyMessage="No servers yet"
items={sortedItems}
key={(x) => x}
current={current()}
onSelect={(x) => {
if (x) select(x)
}}
>
{(i) => (
<div
class="flex items-center gap-2 min-w-0 flex-1"
classList={{ "opacity-50": store.status[i]?.healthy === false }}
>
<div
classList={{
"size-1.5 rounded-full shrink-0": true,
"bg-icon-success-base": store.status[i]?.healthy === true,
"bg-icon-critical-base": store.status[i]?.healthy === false,
"bg-border-weak-base": store.status[i] === undefined,
}}
/>
<span class="truncate">{serverDisplayName(i)}</span>
<span class="text-text-weak">{store.status[i]?.version}</span>
</div>
)}
</List>
<div class="mt-6 px-3 flex flex-col gap-1.5">
<div class="px-3">
<h3 class="text-14-regular text-text-weak">Add a server</h3>
</div>
<form onSubmit={handleSubmit}>
<div class="flex items-start gap-2">
<div class="flex-1 min-w-0 h-auto">
<TextField
type="text"
label="Server URL"
hideLabel
placeholder="http://localhost:4096"
value={store.url}
onChange={(v) => {
setStore("url", v)
setStore("error", "")
}}
validationState={store.error ? "invalid" : "valid"}
error={store.error}
/>
</div>
<Button type="submit" variant="secondary" icon="plus-small" size="large" disabled={store.adding}>
{store.adding ? "Checking..." : "Add"}
</Button>
</div>
</form>
</div>
</div>
</Dialog>
)
}

View File

@@ -1,211 +0,0 @@
import { useGlobalSync } from "@/context/global-sync"
import { useGlobalSDK } from "@/context/global-sdk"
import { useLayout } from "@/context/layout"
import { Session } from "@opencode-ai/sdk/v2/client"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { Mark } from "@opencode-ai/ui/logo"
import { Popover } from "@opencode-ai/ui/popover"
import { Select } from "@opencode-ai/ui/select"
import { TextField } from "@opencode-ai/ui/text-field"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { base64Decode } from "@opencode-ai/util/encode"
import { useCommand } from "@/context/command"
import { getFilename } from "@opencode-ai/util/path"
import { A, useParams } from "@solidjs/router"
import { createMemo, createResource, Show } from "solid-js"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { iife } from "@opencode-ai/util/iife"
export function Header(props: {
navigateToProject: (directory: string) => void
navigateToSession: (session: Session | undefined) => void
onMobileMenuToggle?: () => void
}) {
const globalSync = useGlobalSync()
const globalSDK = useGlobalSDK()
const layout = useLayout()
const params = useParams()
const command = useCommand()
return (
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
<button
type="button"
class="xl:hidden w-12 shrink-0 flex items-center justify-center border-r border-border-weak-base hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active transition-colors"
onClick={props.onMobileMenuToggle}
>
<Icon name="menu" size="small" />
</button>
<A
href="/"
classList={{
"hidden xl:flex": true,
"w-12 shrink-0 px-4 py-3.5": true,
"items-center justify-start self-stretch": true,
"border-r border-border-weak-base": true,
}}
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
data-tauri-drag-region
>
<Mark class="shrink-0" />
</A>
<div class="pl-4 px-6 flex items-center justify-between gap-4 w-full">
<Show when={layout.projects.list().length > 0 && params.dir}>
{(directory) => {
const currentDirectory = createMemo(() => base64Decode(directory()))
const store = createMemo(() => globalSync.child(currentDirectory())[0])
const sessions = createMemo(() => (store().session ?? []).filter((s) => !s.parentID))
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
const shareEnabled = createMemo(() => store().config.share !== "disabled")
return (
<>
<div class="flex items-center gap-3 min-w-0">
<div class="flex items-center gap-2 min-w-0">
<div class="hidden xl:flex items-center gap-2">
<Select
options={layout.projects.list().map((project) => project.worktree)}
current={currentDirectory()}
label={(x) => getFilename(x)}
onSelect={(x) => (x ? props.navigateToProject(x) : undefined)}
class="text-14-regular text-text-base"
variant="ghost"
>
{/* @ts-ignore */}
{(i) => (
<div class="flex items-center gap-2">
<Icon name="folder" size="small" />
<div class="text-text-strong">{getFilename(i)}</div>
</div>
)}
</Select>
<div class="text-text-weaker">/</div>
</div>
<Select
options={sessions()}
current={currentSession()}
placeholder="New session"
label={(x) => x.title}
value={(x) => x.id}
onSelect={props.navigateToSession}
class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
variant="ghost"
/>
</div>
<Show when={currentSession()}>
<Tooltip
class="hidden xl:block"
value={
<div class="flex items-center gap-2">
<span>New session</span>
<span class="text-icon-base text-12-medium">{command.keybind("session.new")}</span>
</div>
}
>
<Button as={A} href={`/${params.dir}/session`} icon="plus-small">
New session
</Button>
</Tooltip>
</Show>
</div>
<div class="flex items-center gap-4">
<Show when={currentSession()?.summary?.files}>
<Tooltip
class="hidden md:block shrink-0"
value={
<div class="flex items-center gap-2">
<span>Toggle review</span>
<span class="text-icon-base text-12-medium">{command.keybind("review.toggle")}</span>
</div>
}
>
<Button variant="ghost" class="group/review-toggle size-6 p-0" onClick={layout.review.toggle}>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
name={layout.review.opened() ? "layout-right" : "layout-left"}
size="small"
class="group-hover/review-toggle:hidden"
/>
<Icon
name={layout.review.opened() ? "layout-right-partial" : "layout-left-partial"}
size="small"
class="hidden group-hover/review-toggle:inline-block"
/>
<Icon
name={layout.review.opened() ? "layout-right-full" : "layout-left-full"}
size="small"
class="hidden group-active/review-toggle:inline-block"
/>
</div>
</Button>
</Tooltip>
</Show>
<Tooltip
class="hidden md:block shrink-0"
value={
<div class="flex items-center gap-2">
<span>Toggle terminal</span>
<span class="text-icon-base text-12-medium">{command.keybind("terminal.toggle")}</span>
</div>
}
>
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
class="group-hover/terminal-toggle:hidden"
/>
<Icon
size="small"
name="layout-bottom-partial"
class="hidden group-hover/terminal-toggle:inline-block"
/>
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
class="hidden group-active/terminal-toggle:inline-block"
/>
</div>
</Button>
</Tooltip>
<Show when={shareEnabled() && currentSession()}>
<Popover
title="Share session"
trigger={
<Tooltip class="shrink-0" value="Share session">
<IconButton icon="share" variant="ghost" class="" />
</Tooltip>
}
>
{iife(() => {
const [url] = createResource(
() => currentSession(),
async (session) => {
if (!session) return
let shareURL = session.share?.url
if (!shareURL) {
shareURL = await globalSDK.client.session
.share({ sessionID: session.id, directory: currentDirectory() })
.then((r) => r.data?.share?.url)
}
return shareURL
},
)
return (
<Show when={url()}>
{(url) => <TextField value={url()} readOnly copyable class="w-72" />}
</Show>
)
})}
</Popover>
</Show>
</div>
</>
)
}}
</Show>
</div>
</header>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,25 @@
import { createMemo, Show } from "solid-js"
import { Match, Show, Switch, createMemo } from "solid-js"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
import { useSync } from "@/context/sync"
import { Button } from "@opencode-ai/ui/button"
import { useParams } from "@solidjs/router"
import { AssistantMessage } from "@opencode-ai/sdk/v2"
import { AssistantMessage } from "@opencode-ai/sdk/v2/client"
export function SessionContextUsage() {
import { useLayout } from "@/context/layout"
import { useSync } from "@/context/sync"
interface SessionContextUsageProps {
variant?: "button" | "indicator"
}
export function SessionContextUsage(props: SessionContextUsageProps) {
const sync = useSync()
const params = useParams()
const layout = useLayout()
const variant = createMemo(() => props.variant ?? "button")
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const tabs = createMemo(() => layout.tabs(sessionKey()))
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
const cost = createMemo(() => {
@@ -19,7 +31,11 @@ export function SessionContextUsage() {
})
const context = createMemo(() => {
const last = messages().findLast((x) => x.role === "assistant" && x.tokens.output > 0) as AssistantMessage
const last = messages().findLast((x) => {
if (x.role !== "assistant") return false
const total = x.tokens.input + x.tokens.output + x.tokens.reasoning + x.tokens.cache.read + x.tokens.cache.write
return total > 0
}) as AssistantMessage
if (!last) return
const total =
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
@@ -30,34 +46,57 @@ export function SessionContextUsage() {
}
})
return (
<Show when={context?.()}>
{(ctx) => (
<Tooltip
value={
<div class="flex flex-col gap-1">
<div class="flex gap-3">
<span class="opacity-70 text-right flex-1">Tokens</span>
<span class="text-left flex-1">{ctx().tokens}</span>
</div>
<div class="flex gap-3">
<span class="opacity-70 text-right flex-1">Usage</span>
<span class="text-left flex-1">{ctx().percentage ?? 0}%</span>
</div>
<div class="flex gap-3">
<span class="opacity-70 text-right flex-1">Cost</span>
<span class="text-left flex-1">{cost()}</span>
</div>
const openContext = () => {
if (!params.id) return
layout.review.open()
tabs().open("context")
tabs().setActive("context")
}
const circle = () => (
<div class="p-1">
<ProgressCircle size={16} strokeWidth={2} percentage={context()?.percentage ?? 0} />
</div>
)
const tooltipValue = () => (
<div>
<Show when={context()}>
{(ctx) => (
<>
<div class="flex items-center gap-2">
<span class="text-text-invert-strong">{ctx().tokens}</span>
<span class="text-text-invert-base">Tokens</span>
</div>
}
placement="top"
>
<div class="flex items-center gap-1.5">
<ProgressCircle size={16} strokeWidth={2} percentage={ctx().percentage ?? 0} />
{/* <span class="text-12-medium text-text-weak">{`${ctx().percentage ?? 0}%`}</span> */}
</div>
</Tooltip>
)}
<div class="flex items-center gap-2">
<span class="text-text-invert-strong">{ctx().percentage ?? 0}%</span>
<span class="text-text-invert-base">Usage</span>
</div>
</>
)}
</Show>
<div class="flex items-center gap-2">
<span class="text-text-invert-strong">{cost()}</span>
<span class="text-text-invert-base">Cost</span>
</div>
<Show when={variant() === "button"}>
<div class="text-11-regular text-text-invert-base mt-1">Click to view context</div>
</Show>
</div>
)
return (
<Show when={params.id}>
<Tooltip value={tooltipValue()} placement="top">
<Switch>
<Match when={variant() === "indicator"}>{circle()}</Match>
<Match when={true}>
<Button type="button" variant="ghost" class="size-6" onClick={openContext}>
{circle()}
</Button>
</Match>
</Switch>
</Tooltip>
</Show>
)
}

View File

@@ -1,5 +1,4 @@
import { createMemo, Show } from "solid-js"
import { Icon } from "@opencode-ai/ui/icon"
import { useSync } from "@/context/sync"
import { Tooltip } from "@opencode-ai/ui/tooltip"
@@ -24,12 +23,11 @@ export function SessionLspIndicator() {
<Show when={lspStats().total > 0}>
<Tooltip placement="top" value={tooltipContent()}>
<div class="flex items-center gap-1 px-2 cursor-default select-none">
<Icon
name="code"
size="small"
<div
classList={{
"text-icon-critical-base": lspStats().hasError,
"text-icon-success-base": !lspStats().hasError && lspStats().connected > 0,
"size-1.5 rounded-full": true,
"bg-icon-critical-base": lspStats().hasError,
"bg-icon-success-base": !lspStats().hasError && lspStats().connected > 0,
}}
/>
<span class="text-12-regular text-text-weak">{lspStats().connected} LSP</span>

View File

@@ -1,6 +1,5 @@
import { createMemo, Show } from "solid-js"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useSync } from "@/context/sync"
import { DialogSelectMcp } from "@/components/dialog-select-mcp"
@@ -21,12 +20,11 @@ export function SessionMcpIndicator() {
return (
<Show when={mcpStats().total > 0}>
<Button variant="ghost" onClick={() => dialog.show(() => <DialogSelectMcp />)}>
<Icon
name="mcp"
size="small"
<div
classList={{
"text-icon-critical-base": mcpStats().failed,
"text-icon-success-base": !mcpStats().failed && mcpStats().enabled > 0,
"size-1.5 rounded-full": true,
"bg-icon-critical-base": mcpStats().failed,
"bg-icon-success-base": !mcpStats().failed && mcpStats().enabled > 0,
}}
/>
<span class="text-12-regular text-text-weak">{mcpStats().enabled} MCP</span>

View File

@@ -0,0 +1,5 @@
export { SessionHeader } from "./session-header"
export { SessionContextTab } from "./session-context-tab"
export { SortableTab, FileVisual } from "./session-sortable-tab"
export { SortableTerminalTab } from "./session-sortable-terminal-tab"
export { NewSessionView } from "./session-new-view"

View File

@@ -0,0 +1,419 @@
import { createMemo, createEffect, on, onCleanup, For, Show } from "solid-js"
import type { JSX } from "solid-js"
import { useParams } from "@solidjs/router"
import { DateTime } from "luxon"
import { useSync } from "@/context/sync"
import { useLayout } from "@/context/layout"
import { checksum } from "@opencode-ai/util/encode"
import { Icon } from "@opencode-ai/ui/icon"
import { Accordion } from "@opencode-ai/ui/accordion"
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
import { Code } from "@opencode-ai/ui/code"
import { Markdown } from "@opencode-ai/ui/markdown"
import type { AssistantMessage, Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
interface SessionContextTabProps {
messages: () => Message[]
visibleUserMessages: () => UserMessage[]
view: () => ReturnType<ReturnType<typeof useLayout>["view"]>
info: () => ReturnType<ReturnType<typeof useSync>["session"]["get"]>
}
export function SessionContextTab(props: SessionContextTabProps) {
const params = useParams()
const sync = useSync()
const ctx = createMemo(() => {
const last = props.messages().findLast((x) => {
if (x.role !== "assistant") return false
const total = x.tokens.input + x.tokens.output + x.tokens.reasoning + x.tokens.cache.read + x.tokens.cache.write
return total > 0
}) as AssistantMessage
if (!last) return
const provider = sync.data.provider.all.find((x) => x.id === last.providerID)
const model = provider?.models[last.modelID]
const limit = model?.limit.context
const input = last.tokens.input
const output = last.tokens.output
const reasoning = last.tokens.reasoning
const cacheRead = last.tokens.cache.read
const cacheWrite = last.tokens.cache.write
const total = input + output + reasoning + cacheRead + cacheWrite
const usage = limit ? Math.round((total / limit) * 100) : null
return {
message: last,
provider,
model,
limit,
input,
output,
reasoning,
cacheRead,
cacheWrite,
total,
usage,
}
})
const cost = createMemo(() => {
const total = props.messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0)
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(total)
})
const counts = createMemo(() => {
const all = props.messages()
const user = all.reduce((count, x) => count + (x.role === "user" ? 1 : 0), 0)
const assistant = all.reduce((count, x) => count + (x.role === "assistant" ? 1 : 0), 0)
return {
all: all.length,
user,
assistant,
}
})
const systemPrompt = createMemo(() => {
const msg = props.visibleUserMessages().findLast((m) => !!m.system)
const system = msg?.system
if (!system) return
const trimmed = system.trim()
if (!trimmed) return
return trimmed
})
const number = (value: number | null | undefined) => {
if (value === undefined) return "—"
if (value === null) return "—"
return value.toLocaleString()
}
const percent = (value: number | null | undefined) => {
if (value === undefined) return "—"
if (value === null) return "—"
return value.toString() + "%"
}
const time = (value: number | undefined) => {
if (!value) return "—"
return DateTime.fromMillis(value).toLocaleString(DateTime.DATETIME_MED)
}
const providerLabel = createMemo(() => {
const c = ctx()
if (!c) return "—"
return c.provider?.name ?? c.message.providerID
})
const modelLabel = createMemo(() => {
const c = ctx()
if (!c) return "—"
if (c.model?.name) return c.model.name
return c.message.modelID
})
const breakdown = createMemo(
on(
() => [ctx()?.message.id, ctx()?.input, props.messages().length, systemPrompt()],
() => {
const c = ctx()
if (!c) return []
const input = c.input
if (!input) return []
const out = {
system: systemPrompt()?.length ?? 0,
user: 0,
assistant: 0,
tool: 0,
}
for (const msg of props.messages()) {
const parts = (sync.data.part[msg.id] ?? []) as Part[]
if (msg.role === "user") {
for (const part of parts) {
if (part.type === "text") out.user += part.text.length
if (part.type === "file") out.user += part.source?.text.value.length ?? 0
if (part.type === "agent") out.user += part.source?.value.length ?? 0
}
continue
}
if (msg.role === "assistant") {
for (const part of parts) {
if (part.type === "text") out.assistant += part.text.length
if (part.type === "reasoning") out.assistant += part.text.length
if (part.type === "tool") {
out.tool += Object.keys(part.state.input).length * 16
if (part.state.status === "pending") out.tool += part.state.raw.length
if (part.state.status === "completed") out.tool += part.state.output.length
if (part.state.status === "error") out.tool += part.state.error.length
}
}
}
}
const estimateTokens = (chars: number) => Math.ceil(chars / 4)
const system = estimateTokens(out.system)
const user = estimateTokens(out.user)
const assistant = estimateTokens(out.assistant)
const tool = estimateTokens(out.tool)
const estimated = system + user + assistant + tool
const pct = (tokens: number) => (tokens / input) * 100
const pctLabel = (tokens: number) => (Math.round(pct(tokens) * 10) / 10).toString() + "%"
const build = (tokens: { system: number; user: number; assistant: number; tool: number; other: number }) => {
return [
{
key: "system",
label: "System",
tokens: tokens.system,
width: pct(tokens.system),
percent: pctLabel(tokens.system),
color: "var(--syntax-info)",
},
{
key: "user",
label: "User",
tokens: tokens.user,
width: pct(tokens.user),
percent: pctLabel(tokens.user),
color: "var(--syntax-success)",
},
{
key: "assistant",
label: "Assistant",
tokens: tokens.assistant,
width: pct(tokens.assistant),
percent: pctLabel(tokens.assistant),
color: "var(--syntax-property)",
},
{
key: "tool",
label: "Tool Calls",
tokens: tokens.tool,
width: pct(tokens.tool),
percent: pctLabel(tokens.tool),
color: "var(--syntax-warning)",
},
{
key: "other",
label: "Other",
tokens: tokens.other,
width: pct(tokens.other),
percent: pctLabel(tokens.other),
color: "var(--syntax-comment)",
},
].filter((x) => x.tokens > 0)
}
if (estimated <= input) {
return build({ system, user, assistant, tool, other: input - estimated })
}
const scale = input / estimated
const scaled = {
system: Math.floor(system * scale),
user: Math.floor(user * scale),
assistant: Math.floor(assistant * scale),
tool: Math.floor(tool * scale),
}
const scaledTotal = scaled.system + scaled.user + scaled.assistant + scaled.tool
return build({ ...scaled, other: Math.max(0, input - scaledTotal) })
},
),
)
function Stat(statProps: { label: string; value: JSX.Element }) {
return (
<div class="flex flex-col gap-1">
<div class="text-12-regular text-text-weak">{statProps.label}</div>
<div class="text-12-medium text-text-strong">{statProps.value}</div>
</div>
)
}
const stats = createMemo(() => {
const c = ctx()
const count = counts()
return [
{ label: "Session", value: props.info()?.title ?? params.id ?? "—" },
{ label: "Messages", value: count.all.toLocaleString() },
{ label: "Provider", value: providerLabel() },
{ label: "Model", value: modelLabel() },
{ label: "Context Limit", value: number(c?.limit) },
{ label: "Total Tokens", value: number(c?.total) },
{ label: "Usage", value: percent(c?.usage) },
{ label: "Input Tokens", value: number(c?.input) },
{ label: "Output Tokens", value: number(c?.output) },
{ label: "Reasoning Tokens", value: number(c?.reasoning) },
{ label: "Cache Tokens (read/write)", value: `${number(c?.cacheRead)} / ${number(c?.cacheWrite)}` },
{ label: "User Messages", value: count.user.toLocaleString() },
{ label: "Assistant Messages", value: count.assistant.toLocaleString() },
{ label: "Total Cost", value: cost() },
{ label: "Session Created", value: time(props.info()?.time.created) },
{ label: "Last Activity", value: time(c?.message.time.created) },
] satisfies { label: string; value: JSX.Element }[]
})
function RawMessageContent(msgProps: { message: Message }) {
const file = createMemo(() => {
const parts = (sync.data.part[msgProps.message.id] ?? []) as Part[]
const contents = JSON.stringify({ message: msgProps.message, parts }, null, 2)
return {
name: `${msgProps.message.role}-${msgProps.message.id}.json`,
contents,
cacheKey: checksum(contents),
}
})
return <Code file={file()} overflow="wrap" class="select-text" />
}
function RawMessage(msgProps: { message: Message }) {
return (
<Accordion.Item value={msgProps.message.id}>
<StickyAccordionHeader>
<Accordion.Trigger>
<div class="flex items-center justify-between gap-2 w-full">
<div class="min-w-0 truncate">
{msgProps.message.role} <span class="text-text-base"> {msgProps.message.id}</span>
</div>
<div class="flex items-center gap-3">
<div class="shrink-0 text-12-regular text-text-weak">{time(msgProps.message.time.created)}</div>
<Icon name="chevron-grabber-vertical" size="small" class="shrink-0 text-text-weak" />
</div>
</div>
</Accordion.Trigger>
</StickyAccordionHeader>
<Accordion.Content class="bg-background-base">
<div class="p-3">
<RawMessageContent message={msgProps.message} />
</div>
</Accordion.Content>
</Accordion.Item>
)
}
let scroll: HTMLDivElement | undefined
let frame: number | undefined
let pending: { x: number; y: number } | undefined
const restoreScroll = () => {
const el = scroll
if (!el) return
const s = props.view()?.scroll("context")
if (!s) return
if (el.scrollTop !== s.y) el.scrollTop = s.y
if (el.scrollLeft !== s.x) el.scrollLeft = s.x
}
const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => {
pending = {
x: event.currentTarget.scrollLeft,
y: event.currentTarget.scrollTop,
}
if (frame !== undefined) return
frame = requestAnimationFrame(() => {
frame = undefined
const next = pending
pending = undefined
if (!next) return
props.view().setScroll("context", next)
})
}
createEffect(
on(
() => props.messages().length,
() => {
requestAnimationFrame(restoreScroll)
},
{ defer: true },
),
)
onCleanup(() => {
if (frame === undefined) return
cancelAnimationFrame(frame)
})
return (
<div
class="@container h-full overflow-y-auto no-scrollbar pb-10"
ref={(el) => {
scroll = el
restoreScroll()
}}
onScroll={handleScroll}
>
<div class="px-6 pt-4 flex flex-col gap-10">
<div class="grid grid-cols-1 @[32rem]:grid-cols-2 gap-4">
<For each={stats()}>{(stat) => <Stat label={stat.label} value={stat.value} />}</For>
</div>
<Show when={breakdown().length > 0}>
<div class="flex flex-col gap-2">
<div class="text-12-regular text-text-weak">Context Breakdown</div>
<div class="h-2 w-full rounded-full bg-surface-base overflow-hidden flex">
<For each={breakdown()}>
{(segment) => (
<div
class="h-full"
style={{
width: `${segment.width}%`,
"background-color": segment.color,
}}
/>
)}
</For>
</div>
<div class="flex flex-wrap gap-x-3 gap-y-1">
<For each={breakdown()}>
{(segment) => (
<div class="flex items-center gap-1 text-11-regular text-text-weak">
<div class="size-2 rounded-sm" style={{ "background-color": segment.color }} />
<div>{segment.label}</div>
<div class="text-text-weaker">{segment.percent}</div>
</div>
)}
</For>
</div>
<div class="hidden text-11-regular text-text-weaker">
Approximate breakdown of input tokens. "Other" includes tool definitions and overhead.
</div>
</div>
</Show>
<Show when={systemPrompt()}>
{(prompt) => (
<div class="flex flex-col gap-2">
<div class="text-12-regular text-text-weak">System Prompt</div>
<div class="border border-border-base rounded-md bg-surface-base px-3 py-2">
<Markdown text={prompt()} class="text-12-regular" />
</div>
</div>
)}
</Show>
<div class="flex flex-col gap-2">
<div class="text-12-regular text-text-weak">Raw messages</div>
<Accordion multiple>
<For each={props.messages()}>{(message) => <RawMessage message={message} />}</For>
</Accordion>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,212 @@
import { createMemo, createResource, Show } from "solid-js"
import { A, useNavigate, useParams } from "@solidjs/router"
import { useLayout } from "@/context/layout"
import { useCommand } from "@/context/command"
import { useServer } from "@/context/server"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useSync } from "@/context/sync"
import { useGlobalSDK } from "@/context/global-sdk"
import { getFilename } from "@opencode-ai/util/path"
import { base64Encode } from "@opencode-ai/util/encode"
import { iife } from "@opencode-ai/util/iife"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Button } from "@opencode-ai/ui/button"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { Select } from "@opencode-ai/ui/select"
import { Popover } from "@opencode-ai/ui/popover"
import { TextField } from "@opencode-ai/ui/text-field"
import { DialogSelectServer } from "@/components/dialog-select-server"
import { SessionLspIndicator } from "@/components/session-lsp-indicator"
import { SessionMcpIndicator } from "@/components/session-mcp-indicator"
import type { Session } from "@opencode-ai/sdk/v2/client"
export function SessionHeader() {
const globalSDK = useGlobalSDK()
const layout = useLayout()
const params = useParams()
const navigate = useNavigate()
const command = useCommand()
const server = useServer()
const dialog = useDialog()
const sync = useSync()
const sessions = createMemo(() => (sync.data.session ?? []).filter((s) => !s.parentID))
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
const branch = createMemo(() => sync.data.vcs?.branch)
function navigateToProject(directory: string) {
navigate(`/${base64Encode(directory)}`)
}
function navigateToSession(session: Session | undefined) {
if (!session) return
navigate(`/${params.dir}/session/${session.id}`)
}
return (
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
<button
type="button"
class="xl:hidden w-12 shrink-0 flex items-center justify-center border-r border-border-weak-base hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active transition-colors"
onClick={layout.mobileSidebar.toggle}
>
<Icon name="menu" size="small" />
</button>
<div class="px-4 flex items-center justify-between gap-4 w-full">
<div class="flex items-center gap-3 min-w-0">
<div class="flex items-center gap-2 min-w-0">
<div class="hidden xl:flex items-center gap-2">
<Select
options={layout.projects.list().map((project) => project.worktree)}
current={sync.directory}
label={(x) => {
const name = getFilename(x)
const b = x === sync.directory ? branch() : undefined
return b ? `${name}:${b}` : name
}}
onSelect={(x) => (x ? navigateToProject(x) : undefined)}
class="text-14-regular text-text-base"
variant="ghost"
>
{/* @ts-ignore */}
{(i) => (
<div class="flex items-center gap-2">
<Icon name="folder" size="small" />
<div class="text-text-strong">{getFilename(i)}</div>
</div>
)}
</Select>
<div class="text-text-weaker">/</div>
</div>
<Select
options={sessions()}
current={currentSession()}
placeholder="New session"
label={(x) => x.title}
value={(x) => x.id}
onSelect={navigateToSession}
class="text-14-regular text-text-base max-w-[calc(100vw-180px)] md:max-w-md"
variant="ghost"
/>
</div>
<Show when={currentSession()}>
<TooltipKeybind class="hidden xl:block" title="New session" keybind={command.keybind("session.new")}>
<IconButton as={A} href={`/${params.dir}/session`} icon="edit-small-2" variant="ghost" />
</TooltipKeybind>
</Show>
</div>
<div class="flex items-center gap-3">
<div class="hidden md:flex items-center gap-1">
<Button
size="small"
variant="ghost"
onClick={() => {
dialog.show(() => <DialogSelectServer />)
}}
>
<div
classList={{
"size-1.5 rounded-full": true,
"bg-icon-success-base": server.healthy() === true,
"bg-icon-critical-base": server.healthy() === false,
"bg-border-weak-base": server.healthy() === undefined,
}}
/>
<Icon name="server" size="small" class="text-icon-weak" />
<span class="text-12-regular text-text-weak truncate max-w-[200px]">{server.name}</span>
</Button>
<SessionLspIndicator />
<SessionMcpIndicator />
</div>
<div class="flex items-center gap-1">
<Show when={currentSession()?.summary?.files}>
<TooltipKeybind
class="hidden md:block shrink-0"
title="Toggle review"
keybind={command.keybind("review.toggle")}
>
<Button variant="ghost" class="group/review-toggle size-6 p-0" onClick={layout.review.toggle}>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
name={layout.review.opened() ? "layout-right" : "layout-left"}
size="small"
class="group-hover/review-toggle:hidden"
/>
<Icon
name={layout.review.opened() ? "layout-right-partial" : "layout-left-partial"}
size="small"
class="hidden group-hover/review-toggle:inline-block"
/>
<Icon
name={layout.review.opened() ? "layout-right-full" : "layout-left-full"}
size="small"
class="hidden group-active/review-toggle:inline-block"
/>
</div>
</Button>
</TooltipKeybind>
</Show>
<TooltipKeybind
class="hidden md:block shrink-0"
title="Toggle terminal"
keybind={command.keybind("terminal.toggle")}
>
<Button variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={layout.terminal.toggle}>
<div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom-full" : "layout-bottom"}
class="group-hover/terminal-toggle:hidden"
/>
<Icon
size="small"
name="layout-bottom-partial"
class="hidden group-hover/terminal-toggle:inline-block"
/>
<Icon
size="small"
name={layout.terminal.opened() ? "layout-bottom" : "layout-bottom-full"}
class="hidden group-active/terminal-toggle:inline-block"
/>
</div>
</Button>
</TooltipKeybind>
</div>
<Show when={shareEnabled() && currentSession()}>
<Popover
title="Share session"
trigger={
<Tooltip class="shrink-0" value="Share session">
<IconButton icon="share" variant="ghost" class="" />
</Tooltip>
}
>
{iife(() => {
const [url] = createResource(
() => currentSession(),
async (session) => {
if (!session) return
let shareURL = session.share?.url
if (!shareURL) {
shareURL = await globalSDK.client.session
.share({ sessionID: session.id, directory: sync.directory })
.then((r) => r.data?.share?.url)
.catch((e) => {
console.error("Failed to share session", e)
return undefined
})
}
return shareURL
},
)
return <Show when={url()}>{(url) => <TextField value={url()} readOnly copyable class="w-72" />}</Show>
})}
</Popover>
</Show>
</div>
</div>
</header>
)
}

View File

@@ -0,0 +1,35 @@
import { Show } from "solid-js"
import { DateTime } from "luxon"
import { useSync } from "@/context/sync"
import { Icon } from "@opencode-ai/ui/icon"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
export function NewSessionView() {
const sync = useSync()
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">
<div class="text-20-medium text-text-weaker">New session</div>
<div class="flex justify-center items-center gap-3">
<Icon name="folder" size="small" />
<div class="text-12-medium text-text-weak">
{getDirectory(sync.data.path.directory)}
<span class="text-text-strong">{getFilename(sync.data.path.directory)}</span>
</div>
</div>
<Show when={sync.project}>
{(project) => (
<div class="flex justify-center items-center gap-3">
<Icon name="pencil-line" size="small" />
<div class="text-12-medium text-text-weak">
Last modified&nbsp;
<span class="text-text-strong">
{DateTime.fromMillis(project().time.updated ?? project().time.created).toRelative()}
</span>
</div>
</div>
)}
</Show>
</div>
)
}

View File

@@ -0,0 +1,48 @@
import { createMemo, Show } from "solid-js"
import type { JSX } from "solid-js"
import { createSortable } from "@thisbeyond/solid-dnd"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Tabs } from "@opencode-ai/ui/tabs"
import { getFilename } from "@opencode-ai/util/path"
import { useFile } from "@/context/file"
export function FileVisual(props: { path: string; active?: boolean }): JSX.Element {
return (
<div class="flex items-center gap-x-1.5">
<FileIcon
node={{ path: props.path, type: "file" }}
classList={{
"grayscale-100 group-data-[selected]/tab:grayscale-0": !props.active,
"grayscale-0": props.active,
}}
/>
<span class="text-14-medium">{getFilename(props.path)}</span>
</div>
)
}
export function SortableTab(props: { tab: string; onTabClose: (tab: string) => void }): JSX.Element {
const file = useFile()
const sortable = createSortable(props.tab)
const path = createMemo(() => file.pathFromTab(props.tab))
return (
// @ts-ignore
<div use:sortable classList={{ "h-full": true, "opacity-0": sortable.isActiveDraggable }}>
<div class="relative h-full">
<Tabs.Trigger
value={props.tab}
closeButton={
<Tooltip value="Close tab" placement="bottom">
<IconButton icon="close" variant="ghost" onClick={() => props.onTabClose(props.tab)} />
</Tooltip>
}
hideCloseButton
>
<Show when={path()}>{(p) => <FileVisual path={p()} />}</Show>
</Tabs.Trigger>
</div>
</div>
)
}

View File

@@ -0,0 +1,27 @@
import type { JSX } from "solid-js"
import { createSortable } from "@thisbeyond/solid-dnd"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tabs } from "@opencode-ai/ui/tabs"
import { useTerminal, type LocalPTY } from "@/context/terminal"
export function SortableTerminalTab(props: { terminal: LocalPTY }): JSX.Element {
const terminal = useTerminal()
const sortable = createSortable(props.terminal.id)
return (
// @ts-ignore
<div use:sortable classList={{ "h-full": true, "opacity-0": sortable.isActiveDraggable }}>
<div class="relative h-full">
<Tabs.Trigger
value={props.terminal.id}
closeButton={
terminal.all().length > 1 && (
<IconButton icon="close" variant="ghost" onClick={() => terminal.close(props.terminal.id)} />
)
}
>
{props.terminal.title}
</Tabs.Trigger>
</div>
</div>
)
}

View File

@@ -1,14 +0,0 @@
import { Show, type ParentProps } from "solid-js"
import { usePlatform } from "@/context/platform"
export function StatusBar(props: ParentProps) {
const platform = usePlatform()
return (
<div class="h-8 w-full shrink-0 flex items-center justify-between px-2 border-t border-border-weak-base bg-background-base">
<Show when={platform.version}>
<span class="text-12-regular text-text-weak">v{platform.version}</span>
</Show>
<div class="flex items-center">{props.children}</div>
</div>
)
}

View File

@@ -1,9 +1,9 @@
import { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
import { ComponentProps, onCleanup, onMount, splitProps } from "solid-js"
import { ComponentProps, createEffect, createSignal, onCleanup, onMount, splitProps } from "solid-js"
import { useSDK } from "@/context/sdk"
import { SerializeAddon } from "@/addons/serialize"
import { LocalPTY } from "@/context/terminal"
import { usePrefersDark } from "@solid-primitives/media"
import { resolveThemeVariant, useTheme } from "@opencode-ai/ui/theme"
export interface TerminalProps extends ComponentProps<"div"> {
pty: LocalPTY
@@ -12,8 +12,28 @@ export interface TerminalProps extends ComponentProps<"div"> {
onConnectError?: (error: unknown) => void
}
type TerminalColors = {
background: string
foreground: string
cursor: string
}
const DEFAULT_TERMINAL_COLORS: Record<"light" | "dark", TerminalColors> = {
light: {
background: "#fcfcfc",
foreground: "#211e1e",
cursor: "#211e1e",
},
dark: {
background: "#191515",
foreground: "#d4d4d4",
cursor: "#d4d4d4",
},
}
export const Terminal = (props: TerminalProps) => {
const sdk = useSDK()
const theme = useTheme()
let container!: HTMLDivElement
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"])
let ws: WebSocket
@@ -22,7 +42,64 @@ export const Terminal = (props: TerminalProps) => {
let serializeAddon: SerializeAddon
let fitAddon: FitAddon
let handleResize: () => void
const prefersDark = usePrefersDark()
const getTerminalColors = (): TerminalColors => {
const mode = theme.mode()
const fallback = DEFAULT_TERMINAL_COLORS[mode]
const currentTheme = theme.themes()[theme.themeId()]
if (!currentTheme) return fallback
const variant = mode === "dark" ? currentTheme.dark : currentTheme.light
if (!variant?.seeds) return fallback
const resolved = resolveThemeVariant(variant, mode === "dark")
const text = resolved["text-base"] ?? fallback.foreground
const background = resolved["background-stronger"] ?? fallback.background
return {
background,
foreground: text,
cursor: text,
}
}
const [terminalColors, setTerminalColors] = createSignal<TerminalColors>(getTerminalColors())
createEffect(() => {
const colors = getTerminalColors()
setTerminalColors(colors)
if (!term) return
const setOption = (term as unknown as { setOption?: (key: string, value: TerminalColors) => void }).setOption
if (!setOption) return
setOption("theme", colors)
})
const focusTerminal = () => term?.focus()
const copySelection = () => {
if (!term || !term.hasSelection()) return false
const selection = term.getSelection()
if (!selection) return false
const clipboard = navigator.clipboard
if (clipboard?.writeText) {
clipboard.writeText(selection).catch(() => {})
return true
}
if (!document.body) return false
const textarea = document.createElement("textarea")
textarea.value = selection
textarea.setAttribute("readonly", "")
textarea.style.position = "fixed"
textarea.style.opacity = "0"
document.body.appendChild(textarea)
textarea.select()
const copied = document.execCommand("copy")
document.body.removeChild(textarea)
return copied
}
const handlePointerDown = () => {
const activeElement = document.activeElement
if (activeElement instanceof HTMLElement && activeElement !== container) {
activeElement.blur()
}
focusTerminal()
}
onMount(async () => {
ghostty = await Ghostty.load()
@@ -33,23 +110,22 @@ export const Terminal = (props: TerminalProps) => {
fontSize: 14,
fontFamily: "IBM Plex Mono, monospace",
allowTransparency: true,
theme: prefersDark()
? {
background: "#191515",
foreground: "#d4d4d4",
cursor: "#d4d4d4",
}
: {
background: "#fcfcfc",
foreground: "#211e1e",
cursor: "#211e1e",
},
theme: terminalColors(),
scrollback: 10_000,
ghostty,
})
term.attachCustomKeyEventHandler((event) => {
const key = event.key.toLowerCase()
if (key === "c") {
const macCopy = event.metaKey && !event.ctrlKey && !event.altKey
const linuxCopy = event.ctrlKey && event.shiftKey && !event.metaKey
if ((macCopy || linuxCopy) && copySelection()) {
event.preventDefault()
return true
}
}
// allow for ctrl-` to toggle terminal in parent
if (event.ctrlKey && event.key.toLowerCase() === "`") {
if (event.ctrlKey && key === "`") {
event.preventDefault()
return true
}
@@ -62,6 +138,8 @@ export const Terminal = (props: TerminalProps) => {
term.loadAddon(fitAddon)
term.open(container)
container.addEventListener("pointerdown", handlePointerDown)
focusTerminal()
if (local.pty.buffer) {
if (local.pty.rows && local.pty.cols) {
@@ -75,20 +153,20 @@ export const Terminal = (props: TerminalProps) => {
fitAddon.fit()
}
container.focus()
fitAddon.observeResize()
handleResize = () => fitAddon.fit()
window.addEventListener("resize", handleResize)
term.onResize(async (size) => {
if (ws && ws.readyState === WebSocket.OPEN) {
await sdk.client.pty.update({
ptyID: local.pty.id,
size: {
cols: size.cols,
rows: size.rows,
},
})
await sdk.client.pty
.update({
ptyID: local.pty.id,
size: {
cols: size.cols,
rows: size.rows,
},
})
.catch(() => {})
}
})
term.onData((data) => {
@@ -106,13 +184,15 @@ export const Terminal = (props: TerminalProps) => {
// })
ws.addEventListener("open", () => {
console.log("WebSocket connected")
sdk.client.pty.update({
ptyID: local.pty.id,
size: {
cols: term.cols,
rows: term.rows,
},
})
sdk.client.pty
.update({
ptyID: local.pty.id,
size: {
cols: term.cols,
rows: term.rows,
},
})
.catch(() => {})
})
ws.addEventListener("message", (event) => {
term.write(event.data)
@@ -130,6 +210,7 @@ export const Terminal = (props: TerminalProps) => {
if (handleResize) {
window.removeEventListener("resize", handleResize)
}
container.removeEventListener("pointerdown", handlePointerDown)
if (serializeAddon && props.onCleanup) {
const buffer = serializeAddon.serialize()
props.onCleanup({
@@ -149,8 +230,10 @@ export const Terminal = (props: TerminalProps) => {
ref={container}
data-component="terminal"
data-prevent-autofocus
style={{ "background-color": terminalColors().background }}
classList={{
...(local.classList ?? {}),
"select-text": true,
"size-full px-6 py-3 font-mono": true,
[local.class ?? ""]: !!local.class,
}}

View File

@@ -26,6 +26,7 @@ export interface CommandOption {
suggested?: boolean
disabled?: boolean
onSelect?: (source?: "palette" | "keybind" | "slash") => void
onHighlight?: () => (() => void) | void
}
export function parseKeybind(config: string): Keybind[] {
@@ -115,6 +116,28 @@ export function formatKeybind(config: string): string {
function DialogCommand(props: { options: CommandOption[] }) {
const dialog = useDialog()
let cleanup: (() => void) | void
let committed = false
const handleMove = (option: CommandOption | undefined) => {
cleanup?.()
cleanup = option?.onHighlight?.()
}
const handleSelect = (option: CommandOption | undefined) => {
if (option) {
committed = true
cleanup = undefined
dialog.close()
option.onSelect?.("palette")
}
}
onCleanup(() => {
if (!committed) {
cleanup?.()
}
})
return (
<Dialog title="Commands">
@@ -125,12 +148,8 @@ function DialogCommand(props: { options: CommandOption[] }) {
key={(x) => x?.id}
filterKeys={["title", "description", "category"]}
groupBy={(x) => x.category ?? ""}
onSelect={(option) => {
if (option) {
dialog.close()
option.onSelect?.("palette")
}
}}
onMove={handleMove}
onSelect={handleSelect}
>
{(option) => (
<div class="w-full flex items-center justify-between gap-4">

View File

@@ -0,0 +1,282 @@
import { createMemo, 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 { showToast } from "@opencode-ai/ui/toast"
import { useParams } from "@solidjs/router"
import { getFilename } from "@opencode-ai/util/path"
import { useSDK } from "./sdk"
import { useSync } from "./sync"
import { persisted } from "@/utils/persist"
export type FileSelection = {
startLine: number
startChar: number
endLine: number
endChar: number
}
export type SelectedLineRange = {
start: number
end: number
side?: "additions" | "deletions"
endSide?: "additions" | "deletions"
}
export type FileViewState = {
scrollTop?: number
scrollLeft?: number
selectedLines?: SelectedLineRange | null
}
export type FileState = {
path: string
name: string
loaded?: boolean
loading?: boolean
error?: string
content?: FileContent
}
function stripFileProtocol(input: string) {
if (!input.startsWith("file://")) return input
return input.slice("file://".length)
}
function stripQueryAndHash(input: string) {
const hashIndex = input.indexOf("#")
const queryIndex = input.indexOf("?")
if (hashIndex !== -1 && queryIndex !== -1) {
return input.slice(0, Math.min(hashIndex, queryIndex))
}
if (hashIndex !== -1) return input.slice(0, hashIndex)
if (queryIndex !== -1) return input.slice(0, queryIndex)
return input
}
export function selectionFromLines(range: SelectedLineRange): FileSelection {
const startLine = Math.min(range.start, range.end)
const endLine = Math.max(range.start, range.end)
return {
startLine,
endLine,
startChar: 0,
endChar: 0,
}
}
function normalizeSelectedLines(range: SelectedLineRange): SelectedLineRange {
if (range.start <= range.end) return range
const startSide = range.side
const endSide = range.endSide ?? startSide
return {
...range,
start: range.end,
end: range.start,
side: endSide,
endSide: startSide !== endSide ? startSide : undefined,
}
}
export const { use: useFile, provider: FileProvider } = createSimpleContext({
name: "File",
init: () => {
const sdk = useSDK()
const sync = useSync()
const params = useParams()
const directory = createMemo(() => sync.data.path.directory)
function normalize(input: string) {
const root = directory()
const prefix = root.endsWith("/") ? root : root + "/"
let path = stripQueryAndHash(stripFileProtocol(input))
if (path.startsWith(prefix)) {
path = path.slice(prefix.length)
}
if (path.startsWith(root)) {
path = path.slice(root.length)
}
if (path.startsWith("./")) {
path = path.slice(2)
}
if (path.startsWith("/")) {
path = path.slice(1)
}
return path
}
function tab(input: string) {
const path = normalize(input)
return `file://${path}`
}
function pathFromTab(tabValue: string) {
if (!tabValue.startsWith("file://")) return
return normalize(tabValue)
}
const inflight = new Map<string, Promise<void>>()
const [store, setStore] = createStore<{
file: Record<string, FileState>
}>({
file: {},
})
const viewKey = createMemo(() => `${params.dir}/file${params.id ? "/" + params.id : ""}.v1`)
const [view, setView, _, ready] = persisted(
viewKey(),
createStore<{
file: Record<string, FileViewState>
}>({
file: {},
}),
)
function ensure(path: string) {
if (!path) return
if (store.file[path]) return
setStore("file", path, { path, name: getFilename(path) })
}
function load(input: string, options?: { force?: boolean }) {
const path = normalize(input)
if (!path) return Promise.resolve()
ensure(path)
const current = store.file[path]
if (!options?.force && current?.loaded) return Promise.resolve()
const pending = inflight.get(path)
if (pending) return pending
setStore(
"file",
path,
produce((draft) => {
draft.loading = true
draft.error = undefined
}),
)
const promise = sdk.client.file
.read({ path })
.then((x) => {
setStore(
"file",
path,
produce((draft) => {
draft.loaded = true
draft.loading = false
draft.content = x.data
}),
)
})
.catch((e) => {
setStore(
"file",
path,
produce((draft) => {
draft.loading = false
draft.error = e.message
}),
)
showToast({
variant: "error",
title: "Failed to load file",
description: e.message,
})
})
.finally(() => {
inflight.delete(path)
})
inflight.set(path, promise)
return promise
}
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 })
})
const get = (input: string) => store.file[normalize(input)]
const scrollTop = (input: string) => view.file[normalize(input)]?.scrollTop
const scrollLeft = (input: string) => view.file[normalize(input)]?.scrollLeft
const selectedLines = (input: string) => view.file[normalize(input)]?.selectedLines
const setScrollTop = (input: string, top: number) => {
const path = normalize(input)
setView("file", path, (current) => {
if (current?.scrollTop === top) return current
return {
...(current ?? {}),
scrollTop: top,
}
})
}
const setScrollLeft = (input: string, left: number) => {
const path = normalize(input)
setView("file", path, (current) => {
if (current?.scrollLeft === left) return current
return {
...(current ?? {}),
scrollLeft: left,
}
})
}
const setSelectedLines = (input: string, range: SelectedLineRange | null) => {
const path = normalize(input)
const next = range ? normalizeSelectedLines(range) : null
setView("file", path, (current) => {
if (current?.selectedLines === next) return current
return {
...(current ?? {}),
selectedLines: next,
}
})
}
onCleanup(() => stop())
return {
ready,
normalize,
tab,
pathFromTab,
get,
load,
scrollTop,
scrollLeft,
setScrollTop,
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)),
}
},
})

View File

@@ -1,34 +1,40 @@
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2/client"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { onCleanup } from "solid-js"
import { usePlatform } from "./platform"
import { useServer } from "./server"
export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({
name: "GlobalSDK",
init: (props: { url: string }) => {
init: () => {
const server = useServer()
const abort = new AbortController()
const eventSdk = createOpencodeClient({
baseUrl: props.url,
// signal: AbortSignal.timeout(1000 * 60 * 10),
baseUrl: server.url,
signal: abort.signal,
})
const emitter = createGlobalEmitter<{
[key: string]: Event
}>()
eventSdk.global.event().then(async (events) => {
void (async () => {
const events = await eventSdk.global.event()
for await (const event of events.stream) {
// console.log("event", event)
emitter.emit(event.directory ?? "global", event.payload)
}
})
})().catch(() => undefined)
onCleanup(() => abort.abort())
const platform = usePlatform()
const sdk = createOpencodeClient({
baseUrl: props.url,
signal: AbortSignal.timeout(1000 * 60 * 10),
baseUrl: server.url,
fetch: platform.fetch,
throwOnError: true,
})
return { url: props.url, client: sdk, event: emitter }
return { url: server.url, client: sdk, event: emitter }
},
})

View File

@@ -14,6 +14,8 @@ import {
type Command,
type McpStatus,
type LspStatus,
type VcsInfo,
type PermissionRequest,
createOpencodeClient,
} from "@opencode-ai/sdk/v2/client"
import { createStore, produce, reconcile } from "solid-js/store"
@@ -21,12 +23,12 @@ 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 { createContext, useContext, onMount, type ParentProps, Switch, Match } from "solid-js"
import { batch, createContext, useContext, onMount, type ParentProps, Switch, Match } from "solid-js"
import { showToast } from "@opencode-ai/ui/toast"
import { getFilename } from "@opencode-ai/util/path"
type State = {
ready: boolean
status: "loading" | "partial" | "complete"
agent: Agent[]
command: Command[]
project: string
@@ -43,10 +45,14 @@ type State = {
todo: {
[sessionID: string]: Todo[]
}
permission: {
[sessionID: string]: PermissionRequest[]
}
mcp: {
[name: string]: McpStatus
}
lsp: LspStatus[]
vcs: VcsInfo | undefined
limit: number
message: {
[sessionID: string]: Message[]
@@ -65,39 +71,38 @@ function createGlobalSync() {
project: Project[]
provider: ProviderListResponse
provider_auth: ProviderAuthResponse
children: Record<string, State>
}>({
ready: false,
path: { state: "", config: "", worktree: "", directory: "", home: "" },
project: [],
provider: { all: [], connected: [], default: {} },
provider_auth: {},
children: {},
})
const children: Record<string, ReturnType<typeof createStore<State>>> = {}
function child(directory: string) {
if (!directory) console.error("No directory provided")
if (!children[directory]) {
setGlobalStore("children", directory, {
children[directory] = createStore<State>({
project: "",
provider: { all: [], connected: [], default: {} },
config: {},
path: { state: "", config: "", worktree: "", directory: "", home: "" },
ready: false,
status: "loading" as const,
agent: [],
command: [],
session: [],
session_status: {},
session_diff: {},
todo: {},
permission: {},
mcp: {},
lsp: [],
vcs: undefined,
limit: 5,
message: {},
part: {},
})
children[directory] = createStore(globalStore.children[directory])
bootstrapInstance(directory)
}
return children[directory]
@@ -110,16 +115,17 @@ function createGlobalSync() {
.then((x) => {
const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000
const nonArchived = (x.data ?? [])
.filter((s) => !!s?.id)
.filter((s) => !s.time?.archived)
.slice()
.filter((s) => !s.time.archived)
.sort((a, b) => a.id.localeCompare(b.id))
// Include up to the limit, plus any updated in the last 4 hours
const sessions = nonArchived.filter((s, i) => {
if (i < store.limit) return true
const updated = new Date(s.time.updated).getTime()
const updated = new Date(s.time?.updated ?? s.time?.created).getTime()
return updated > fourHoursAgo
})
setStore("session", sessions)
setStore("session", reconcile(sessions, { key: "id" }))
})
.catch((err) => {
console.error("Failed to load sessions", err)
@@ -130,13 +136,14 @@ function createGlobalSync() {
async function bootstrapInstance(directory: string) {
if (!directory) return
const [, setStore] = child(directory)
const [store, setStore] = child(directory)
const sdk = createOpencodeClient({
baseUrl: globalSDK.url,
directory,
throwOnError: true,
})
const load = {
const blockingRequests = {
project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
provider: () =>
sdk.provider.list().then((x) => {
@@ -151,17 +158,57 @@ function createGlobalSync() {
})),
})
}),
path: () => sdk.path.get().then((x) => setStore("path", x.data!)),
agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
command: () => sdk.command.list().then((x) => setStore("command", x.data ?? [])),
session: () => loadSessions(directory),
status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)),
config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
mcp: () => sdk.mcp.status().then((x) => setStore("mcp", x.data ?? {})),
lsp: () => sdk.lsp.status().then((x) => setStore("lsp", x.data ?? [])),
}
await Promise.all(Object.values(load).map((p) => retry(p).catch((e) => setGlobalStore("error", e))))
.then(() => setStore("ready", true))
await Promise.all(Object.values(blockingRequests).map((p) => retry(p).catch((e) => setGlobalStore("error", e))))
.then(() => {
if (store.status !== "complete") setStore("status", "partial")
// non-blocking
Promise.all([
sdk.path.get().then((x) => setStore("path", x.data!)),
sdk.command.list().then((x) => setStore("command", x.data ?? [])),
sdk.session.status().then((x) => setStore("session_status", x.data!)),
loadSessions(directory),
sdk.mcp.status().then((x) => setStore("mcp", x.data!)),
sdk.lsp.status().then((x) => setStore("lsp", x.data!)),
sdk.vcs.get().then((x) => setStore("vcs", x.data)),
sdk.permission.list().then((x) => {
const grouped: Record<string, PermissionRequest[]> = {}
for (const perm of x.data ?? []) {
if (!perm?.id || !perm.sessionID) continue
const existing = grouped[perm.sessionID]
if (existing) {
existing.push(perm)
continue
}
grouped[perm.sessionID] = [perm]
}
batch(() => {
for (const sessionID of Object.keys(store.permission)) {
if (grouped[sessionID]) continue
setStore("permission", sessionID, [])
}
for (const [sessionID, permissions] of Object.entries(grouped)) {
setStore(
"permission",
sessionID,
reconcile(
permissions
.filter((p) => !!p?.id)
.slice()
.sort((a, b) => a.id.localeCompare(b.id)),
{ key: "id" },
),
)
}
})
}),
]).then(() => {
setStore("status", "complete")
})
})
.catch((e) => setGlobalStore("error", e))
}
@@ -228,7 +275,7 @@ function createGlobalSync() {
setStore("session_diff", event.properties.sessionID, reconcile(event.properties.diff, { key: "file" }))
break
case "todo.updated":
setStore("todo", event.properties.sessionID, reconcile(event.properties.todos))
setStore("todo", event.properties.sessionID, reconcile(event.properties.todos, { key: "id" }))
break
case "session.status": {
setStore("session_status", event.properties.sessionID, reconcile(event.properties.status))
@@ -305,6 +352,56 @@ function createGlobalSync() {
}
break
}
case "vcs.branch.updated": {
setStore("vcs", { branch: event.properties.branch })
break
}
case "permission.asked": {
const sessionID = event.properties.sessionID
const permissions = store.permission[sessionID]
if (!permissions) {
setStore("permission", sessionID, [event.properties])
break
}
const result = Binary.search(permissions, event.properties.id, (p) => p.id)
if (result.found) {
setStore("permission", sessionID, result.index, reconcile(event.properties))
break
}
setStore(
"permission",
sessionID,
produce((draft) => {
draft.splice(result.index, 0, event.properties)
}),
)
break
}
case "permission.replied": {
const permissions = store.permission[event.properties.sessionID]
if (!permissions) break
const result = Binary.search(permissions, event.properties.requestID, (p) => p.id)
if (!result.found) break
setStore(
"permission",
event.properties.sessionID,
produce((draft) => {
draft.splice(result.index, 1)
}),
)
break
}
case "lsp.updated": {
const sdk = createOpencodeClient({
baseUrl: globalSDK.url,
directory,
throwOnError: true,
})
sdk.lsp.status().then((x) => setStore("lsp", x.data ?? []))
break
}
}
})
@@ -329,10 +426,12 @@ function createGlobalSync() {
),
retry(() =>
globalSDK.client.project.list().then(async (x) => {
setGlobalStore(
"project",
x.data!.filter((p) => !p.worktree.includes("opencode-test")).sort((a, b) => a.id.localeCompare(b.id)),
)
const projects = (x.data ?? [])
.filter((p) => !!p?.id)
.filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
.slice()
.sort((a, b) => a.id.localeCompare(b.id))
setGlobalStore("project", projects)
}),
),
retry(() =>

View File

@@ -3,6 +3,7 @@ import { batch, createMemo, onMount } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSync } from "./global-sync"
import { useGlobalSDK } from "./global-sdk"
import { useServer } from "./server"
import { Project } from "@opencode-ai/sdk/v2"
import { persisted } from "@/utils/persist"
@@ -22,22 +23,41 @@ export function getAvatarColors(key?: string) {
}
}
function same<T>(a: readonly T[] | undefined, b: readonly T[] | undefined) {
if (a === b) return true
if (!a || !b) return false
if (a.length !== b.length) return false
return a.every((x, i) => x === b[i])
}
type SessionTabs = {
active?: string
all: string[]
}
type SessionScroll = {
x: number
y: number
}
type SessionView = {
scroll: Record<string, SessionScroll>
reviewOpen?: string[]
}
export type LocalProject = Partial<Project> & { worktree: string; expanded: boolean }
export type ReviewDiffStyle = "unified" | "split"
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
name: "Layout",
init: () => {
const globalSdk = useGlobalSDK()
const globalSync = useGlobalSync()
const server = useServer()
const [store, setStore, _, ready] = persisted(
"layout.v3",
"layout.v6",
createStore({
projects: [] as { worktree: string; expanded: boolean }[],
sidebar: {
opened: false,
width: 280,
@@ -48,11 +68,16 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
review: {
opened: true,
diffStyle: "split" as ReviewDiffStyle,
},
session: {
width: 600,
},
mobileSidebar: {
opened: false,
},
sessionTabs: {} as Record<string, SessionTabs>,
sessionView: {} as Record<string, SessionView>,
}),
)
@@ -70,6 +95,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
{
...project,
...(metadata ?? {}),
icon: { url: metadata?.icon?.url, color: metadata?.icon?.color },
},
]
}
@@ -85,12 +111,12 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
return project
}
const enriched = createMemo(() => store.projects.flatMap(enrich))
const enriched = createMemo(() => server.projects.list().flatMap(enrich))
const list = createMemo(() => enriched().flatMap(colorize))
onMount(() => {
Promise.all(
store.projects.map((project) => {
server.projects.list().map((project) => {
return globalSync.project.loadSessions(project.worktree)
}),
)
@@ -101,32 +127,23 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
projects: {
list,
open(directory: string) {
if (store.projects.find((x) => x.worktree === directory)) {
if (server.projects.list().find((x) => x.worktree === directory)) {
return
}
globalSync.project.loadSessions(directory)
setStore("projects", (x) => [{ worktree: directory, expanded: true }, ...x])
server.projects.open(directory)
},
close(directory: string) {
setStore("projects", (x) => x.filter((x) => x.worktree !== directory))
server.projects.close(directory)
},
expand(directory: string) {
const index = store.projects.findIndex((x) => x.worktree === directory)
if (index !== -1) setStore("projects", index, "expanded", true)
server.projects.expand(directory)
},
collapse(directory: string) {
const index = store.projects.findIndex((x) => x.worktree === directory)
if (index !== -1) setStore("projects", index, "expanded", false)
server.projects.collapse(directory)
},
move(directory: string, toIndex: number) {
setStore("projects", (projects) => {
const fromIndex = projects.findIndex((x) => x.worktree === directory)
if (fromIndex === -1 || fromIndex === toIndex) return projects
const result = [...projects]
const [item] = result.splice(fromIndex, 1)
result.splice(toIndex, 0, item)
return result
})
server.projects.move(directory, toIndex)
},
},
sidebar: {
@@ -163,6 +180,14 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
review: {
opened: createMemo(() => store.review?.opened ?? true),
diffStyle: createMemo(() => store.review?.diffStyle ?? "split"),
setDiffStyle(diffStyle: ReviewDiffStyle) {
if (!store.review) {
setStore("review", { opened: true, diffStyle })
return
}
setStore("review", "diffStyle", diffStyle)
},
open() {
setStore("review", "opened", true)
},
@@ -178,11 +203,55 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
resize(width: number) {
if (!store.session) {
setStore("session", { width })
} else {
setStore("session", "width", width)
return
}
setStore("session", "width", width)
},
},
mobileSidebar: {
opened: createMemo(() => store.mobileSidebar?.opened ?? false),
show() {
setStore("mobileSidebar", "opened", true)
},
hide() {
setStore("mobileSidebar", "opened", false)
},
toggle() {
setStore("mobileSidebar", "opened", (x) => !x)
},
},
view(sessionKey: string) {
const s = createMemo(() => store.sessionView[sessionKey] ?? { scroll: {} })
return {
scroll(tab: string) {
return s().scroll?.[tab]
},
setScroll(tab: string, pos: SessionScroll) {
const current = store.sessionView[sessionKey]
if (!current) {
setStore("sessionView", sessionKey, { scroll: { [tab]: pos } })
return
}
const prev = current.scroll?.[tab]
if (prev?.x === pos.x && prev?.y === pos.y) return
setStore("sessionView", sessionKey, "scroll", tab, pos)
},
review: {
open: createMemo(() => s().reviewOpen),
setOpen(open: string[]) {
const current = store.sessionView[sessionKey]
if (!current) {
setStore("sessionView", sessionKey, { scroll: {}, reviewOpen: open })
return
}
if (same(current.reviewOpen, open)) return
setStore("sessionView", sessionKey, "reviewOpen", open)
},
},
}
},
tabs(sessionKey: string) {
const tabs = createMemo(() => store.sessionTabs[sessionKey] ?? { all: [] })
return {
@@ -205,38 +274,55 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
async open(tab: string) {
const current = store.sessionTabs[sessionKey] ?? { all: [] }
if (tab !== "review") {
if (!current.all.includes(tab)) {
if (!store.sessionTabs[sessionKey]) {
setStore("sessionTabs", sessionKey, { all: [tab], active: tab })
} else {
setStore("sessionTabs", sessionKey, "all", [...current.all, tab])
setStore("sessionTabs", sessionKey, "active", tab)
}
if (tab === "review") {
if (!store.sessionTabs[sessionKey]) {
setStore("sessionTabs", sessionKey, { all: [], active: tab })
return
}
}
if (!store.sessionTabs[sessionKey]) {
setStore("sessionTabs", sessionKey, { all: [], active: tab })
} else {
setStore("sessionTabs", sessionKey, "active", tab)
return
}
if (tab === "context") {
const all = [tab, ...current.all.filter((x) => x !== tab)]
if (!store.sessionTabs[sessionKey]) {
setStore("sessionTabs", sessionKey, { all, active: tab })
return
}
setStore("sessionTabs", sessionKey, "all", all)
setStore("sessionTabs", sessionKey, "active", tab)
return
}
if (!current.all.includes(tab)) {
if (!store.sessionTabs[sessionKey]) {
setStore("sessionTabs", sessionKey, { all: [tab], active: tab })
return
}
setStore("sessionTabs", sessionKey, "all", [...current.all, tab])
setStore("sessionTabs", sessionKey, "active", tab)
return
}
if (!store.sessionTabs[sessionKey]) {
setStore("sessionTabs", sessionKey, { all: current.all, active: tab })
return
}
setStore("sessionTabs", sessionKey, "active", tab)
},
close(tab: string) {
const current = store.sessionTabs[sessionKey]
if (!current) return
const all = current.all.filter((x) => x !== tab)
batch(() => {
setStore(
"sessionTabs",
sessionKey,
"all",
current.all.filter((x) => x !== tab),
)
if (current.active === tab) {
const index = current.all.findIndex((f) => f === tab)
const previous = current.all[Math.max(0, index - 1)]
setStore("sessionTabs", sessionKey, "active", previous)
}
setStore("sessionTabs", sessionKey, "all", all)
if (current.active !== tab) return
const index = current.all.findIndex((f) => f === tab)
const next = all[index - 1] ?? all[0]
setStore("sessionTabs", sessionKey, "active", next)
})
},
move(tab: string, to: number) {

View File

@@ -65,23 +65,40 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const agent = (() => {
const list = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden))
const [store, setStore] = createStore<{
current: string
current?: string
}>({
current: list()[0].name,
current: list()[0]?.name,
})
return {
list,
current() {
return list().find((x) => x.name === store.current)!
const available = list()
if (available.length === 0) return undefined
return available.find((x) => x.name === store.current) ?? available[0]
},
set(name: string | undefined) {
setStore("current", name ?? list()[0].name)
const available = list()
if (available.length === 0) {
setStore("current", undefined)
return
}
if (name && available.some((x) => x.name === name)) {
setStore("current", name)
return
}
setStore("current", available[0].name)
},
move(direction: 1 | -1) {
let next = list().findIndex((x) => x.name === store.current) + direction
if (next < 0) next = list().length - 1
if (next >= list().length) next = 0
const value = list()[next]
const available = list()
if (available.length === 0) {
setStore("current", undefined)
return
}
let next = available.findIndex((x) => x.name === store.current) + direction
if (next < 0) next = available.length - 1
if (next >= available.length) next = 0
const value = available[next]
if (!value) return
setStore("current", value.name)
if (value.model)
model.set({
@@ -98,9 +115,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
createStore<{
user: (ModelKey & { visibility: "show" | "hide"; favorite?: boolean })[]
recent: ModelKey[]
variant?: Record<string, string | undefined>
}>({
user: [],
recent: [],
variant: {},
}),
)
@@ -182,11 +201,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const current = createMemo(() => {
const a = agent.current()
if (!a) return undefined
const key = getFirstValidModel(
() => ephemeral.model[a.name],
() => a.model,
fallbackModel,
)!
)
if (!key) return undefined
return find(key)
})
@@ -232,7 +253,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
cycle,
set(model: ModelKey | undefined, options?: { recent?: boolean }) {
batch(() => {
setEphemeral("model", agent.current().name, model ?? fallbackModel())
const currentAgent = agent.current()
if (currentAgent) setEphemeral("model", currentAgent.name, model ?? fallbackModel())
if (model) updateVisibility(model, "show")
if (options?.recent && model) {
const uniq = uniqueBy([model, ...store.recent], (x) => x.providerID + x.modelID)
@@ -252,6 +274,45 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
setVisibility(model: ModelKey, visible: boolean) {
updateVisibility(model, visible ? "show" : "hide")
},
variant: {
current() {
const m = current()
if (!m) return undefined
const key = `${m.provider.id}/${m.id}`
return store.variant?.[key]
},
list() {
const m = current()
if (!m) return []
if (!m.variants) return []
return Object.keys(m.variants)
},
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)
}
},
cycle() {
const variants = this.list()
if (variants.length === 0) return
const currentVariant = this.current()
if (!currentVariant) {
this.set(variants[0])
return
}
const index = variants.indexOf(currentVariant)
if (index === -1 || index === variants.length - 1) {
this.set(undefined)
return
}
this.set(variants[index + 1])
},
},
}
})()
@@ -369,7 +430,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
// ]
// })
// setStore("active", relativePath)
context.addActive()
// 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
@@ -377,17 +438,20 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
const list = async (path: string) => {
return sdk.client.file.list({ path: path + "/" }).then((x) => {
setStore(
"node",
produce((draft) => {
x.data!.forEach((node) => {
if (node.path in draft) return
draft[node.path] = node
})
}),
)
})
return sdk.client.file
.list({ path: path + "/" })
.then((x) => {
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!)
@@ -474,66 +538,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
})()
const context = (() => {
const [store, setStore] = createStore<{
activeTab: boolean
files: string[]
activeFile?: string
items: (ContextItem & { key: string })[]
}>({
activeTab: true,
files: [],
items: [],
})
const files = createMemo(() => store.files.map((x) => file.node(x)))
const activeFile = createMemo(() => (store.activeFile ? file.node(store.activeFile) : undefined))
return {
all() {
return store.items
},
// active() {
// return store.activeTab ? file.active() : undefined
// },
addActive() {
setStore("activeTab", true)
},
removeActive() {
setStore("activeTab", false)
},
add(item: ContextItem) {
let key = item.type
switch (item.type) {
case "file":
key += `${item.path}:${item.selection?.startLine}:${item.selection?.endLine}`
break
}
if (store.items.find((x) => x.key === key)) return
setStore("items", (x) => [...x, { key, ...item }])
},
remove(key: string) {
setStore("items", (x) => x.filter((x) => x.key !== key))
},
files,
openFile(path: string) {
file.init(path).then(() => {
setStore("files", (x) => [...x, path])
setStore("activeFile", path)
})
},
activeFile,
setActiveFile(path: string | undefined) {
setStore("activeFile", path)
},
}
})()
const result = {
slug: createMemo(() => base64Encode(sdk.directory)),
model,
agent,
file,
context,
}
return result
},

View File

@@ -2,7 +2,9 @@ import { createStore } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSDK } from "./global-sdk"
import { useGlobalSync } from "./global-sync"
import { usePlatform } from "@/context/platform"
import { Binary } from "@opencode-ai/util/binary"
import { base64Encode } from "@opencode-ai/util/encode"
import { EventSessionError } from "@opencode-ai/sdk/v2"
import { makeAudioPlayer } from "@solid-primitives/audio"
import idleSound from "@opencode-ai/ui/audio/staplebops-01.aac"
@@ -43,6 +45,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
const globalSDK = useGlobalSDK()
const globalSync = useGlobalSync()
const platform = usePlatform()
const [store, setStore, _, ready] = persisted(
"notification.v1",
@@ -64,8 +67,8 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
const sessionID = event.properties.sessionID
const [syncStore] = globalSync.child(directory)
const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
const isChild = match.found && syncStore.session[match.index].parentID
if (isChild) break
const session = match.found ? syncStore.session[match.index] : undefined
if (session?.parentID) break
try {
idlePlayer?.play()
} catch {}
@@ -74,25 +77,29 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
type: "turn-complete",
session: sessionID,
})
const href = `/${base64Encode(directory)}/session/${sessionID}`
void platform.notify("Response ready", session?.title ?? sessionID, href)
break
}
case "session.error": {
const sessionID = event.properties.sessionID
if (sessionID) {
const [syncStore] = globalSync.child(directory)
const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
const isChild = match.found && syncStore.session[match.index].parentID
if (isChild) break
}
const [syncStore] = globalSync.child(directory)
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
try {
errorPlayer?.play()
} catch {}
const error = "error" in event.properties ? event.properties.error : undefined
setStore("list", store.list.length, {
...base,
type: "error",
session: sessionID ?? "global",
error: "error" in event.properties ? event.properties.error : undefined,
error,
})
const description = session?.title ?? (typeof error === "string" ? error : "An error occurred")
const href = sessionID ? `/${base64Encode(directory)}/session/${sessionID}` : `/${base64Encode(directory)}`
void platform.notify("Session error", description, href)
break
}
}

View File

@@ -0,0 +1,122 @@
import { createMemo, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import type { PermissionRequest } from "@opencode-ai/sdk/v2/client"
import { persisted } from "@/utils/persist"
import { useGlobalSDK } from "@/context/global-sdk"
import { useGlobalSync } from "./global-sync"
import { useParams } from "@solidjs/router"
import { base64Decode } from "@opencode-ai/util/encode"
type PermissionRespondFn = (input: {
sessionID: string
permissionID: string
response: "once" | "always" | "reject"
directory?: string
}) => void
function shouldAutoAccept(perm: PermissionRequest) {
return perm.permission === "edit"
}
export const { use: usePermission, provider: PermissionProvider } = createSimpleContext({
name: "Permission",
init: () => {
const params = useParams()
const globalSDK = useGlobalSDK()
const globalSync = useGlobalSync()
const permissionsEnabled = createMemo(() => {
if (!params.dir || !base64Decode(params.dir)) return false
const [store] = globalSync.child(base64Decode(params.dir))
return store.config.permission !== undefined
})
const [store, setStore, _, ready] = persisted(
"permission.v3",
createStore({
autoAcceptEdits: {} as Record<string, boolean>,
}),
)
const responded = new Set<string>()
const respond: PermissionRespondFn = (input) => {
globalSDK.client.permission.respond(input).catch(() => {
responded.delete(input.permissionID)
})
}
function respondOnce(permission: PermissionRequest, directory?: string) {
if (responded.has(permission.id)) return
responded.add(permission.id)
respond({
sessionID: permission.sessionID,
permissionID: permission.id,
response: "once",
directory,
})
}
function isAutoAccepting(sessionID: string) {
return store.autoAcceptEdits[sessionID] ?? false
}
const unsubscribe = globalSDK.event.listen((e) => {
const event = e.details
if (event?.type !== "permission.asked") return
const perm = event.properties
if (!isAutoAccepting(perm.sessionID)) return
if (!shouldAutoAccept(perm)) return
respondOnce(perm, e.name)
})
onCleanup(unsubscribe)
function enable(sessionID: string, directory: string) {
setStore("autoAcceptEdits", sessionID, true)
globalSDK.client.permission
.list({ directory })
.then((x) => {
for (const perm of x.data ?? []) {
if (!perm?.id) continue
if (perm.sessionID !== sessionID) continue
if (!shouldAutoAccept(perm)) continue
respondOnce(perm, directory)
}
})
.catch(() => undefined)
}
function disable(sessionID: string) {
setStore("autoAcceptEdits", sessionID, false)
}
return {
ready,
respond,
autoResponds(permission: PermissionRequest) {
return isAutoAccepting(permission.sessionID) && shouldAutoAccept(permission)
},
isAutoAccepting,
toggleAutoAccept(sessionID: string, directory: string) {
if (isAutoAccepting(sessionID)) {
disable(sessionID)
return
}
enable(sessionID, directory)
},
enableAutoAccept(sessionID: string, directory: string) {
if (isAutoAccepting(sessionID)) return
enable(sessionID, directory)
},
disableAutoAccept(sessionID: string) {
disable(sessionID)
},
permissionsEnabled,
}
},
})

View File

@@ -14,7 +14,10 @@ export type Platform = {
/** Restart the app */
restart(): Promise<void>
/** Open native directory picker dialog (Tauri only) */
/** Send a system notification (optional deep link) */
notify(title: string, description?: string, href?: string): Promise<void>
/** Open directory picker dialog (native on Tauri, server-backed on web) */
openDirectoryPickerDialog?(opts?: { title?: string; multiple?: boolean }): Promise<string | string[] | null>
/** Open native file picker dialog (Tauri only) */

View File

@@ -2,7 +2,7 @@ import { createStore } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { batch, createMemo } from "solid-js"
import { useParams } from "@solidjs/router"
import { TextSelection } from "./local"
import type { FileSelection } from "@/context/file"
import { persisted } from "@/utils/persist"
interface PartBase {
@@ -18,7 +18,12 @@ export interface TextPart extends PartBase {
export interface FileAttachmentPart extends PartBase {
type: "file"
path: string
selection?: TextSelection
selection?: FileSelection
}
export interface AgentPart extends PartBase {
type: "agent"
name: string
}
export interface ImageAttachmentPart {
@@ -29,11 +34,27 @@ export interface ImageAttachmentPart {
dataUrl: string
}
export type ContentPart = TextPart | FileAttachmentPart | ImageAttachmentPart
export type ContentPart = TextPart | FileAttachmentPart | AgentPart | ImageAttachmentPart
export type Prompt = ContentPart[]
export type FileContextItem = {
type: "file"
path: string
selection?: FileSelection
}
export type ContextItem = FileContextItem
export const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
function isSelectionEqual(a?: FileSelection, b?: FileSelection) {
if (!a && !b) return true
if (!a || !b) return false
return (
a.startLine === b.startLine && a.startChar === b.startChar && a.endLine === b.endLine && a.endChar === b.endChar
)
}
export function isPromptEqual(promptA: Prompt, promptB: Prompt): boolean {
if (promptA.length !== promptB.length) return false
for (let i = 0; i < promptA.length; i++) {
@@ -43,7 +64,13 @@ export function isPromptEqual(promptA: Prompt, promptB: Prompt): boolean {
if (partA.type === "text" && partA.content !== (partB as TextPart).content) {
return false
}
if (partA.type === "file" && partA.path !== (partB as FileAttachmentPart).path) {
if (partA.type === "file") {
const fileA = partA as FileAttachmentPart
const fileB = partB as FileAttachmentPart
if (fileA.path !== fileB.path) return false
if (!isSelectionEqual(fileA.selection, fileB.selection)) return false
}
if (partA.type === "agent" && partA.name !== (partB as AgentPart).name) {
return false
}
if (partA.type === "image" && partA.id !== (partB as ImageAttachmentPart).id) {
@@ -53,7 +80,7 @@ export function isPromptEqual(promptA: Prompt, promptB: Prompt): boolean {
return true
}
function cloneSelection(selection?: TextSelection) {
function cloneSelection(selection?: FileSelection) {
if (!selection) return undefined
return { ...selection }
}
@@ -61,6 +88,7 @@ function cloneSelection(selection?: TextSelection) {
function clonePart(part: ContentPart): ContentPart {
if (part.type === "text") return { ...part }
if (part.type === "image") return { ...part }
if (part.type === "agent") return { ...part }
return {
...part,
selection: cloneSelection(part.selection),
@@ -75,24 +103,57 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
name: "Prompt",
init: () => {
const params = useParams()
const name = createMemo(() => `${params.dir}/prompt${params.id ? "/" + params.id : ""}.v1`)
const name = createMemo(() => `${params.dir}/prompt${params.id ? "/" + params.id : ""}.v2`)
const [store, setStore, _, ready] = persisted(
name(),
createStore<{
prompt: Prompt
cursor?: number
context: {
activeTab: boolean
items: (ContextItem & { key: string })[]
}
}>({
prompt: clonePrompt(DEFAULT_PROMPT),
cursor: undefined,
context: {
activeTab: true,
items: [],
},
}),
)
function keyForItem(item: ContextItem) {
if (item.type !== "file") return item.type
const start = item.selection?.startLine
const end = item.selection?.endLine
return `${item.type}:${item.path}:${start}:${end}`
}
return {
ready,
current: createMemo(() => store.prompt),
cursor: createMemo(() => store.cursor),
dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
context: {
activeTab: createMemo(() => store.context.activeTab),
items: createMemo(() => store.context.items),
addActive() {
setStore("context", "activeTab", true)
},
removeActive() {
setStore("context", "activeTab", false)
},
add(item: ContextItem) {
const key = keyForItem(item)
if (store.context.items.find((x) => x.key === key)) return
setStore("context", "items", (items) => [...items, { key, ...item }])
},
remove(key: string) {
setStore("context", "items", (items) => items.filter((x) => x.key !== key))
},
},
set(prompt: Prompt, cursorPosition?: number) {
const next = clonePrompt(prompt)
batch(() => {

View File

@@ -11,7 +11,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
const globalSDK = useGlobalSDK()
const sdk = createOpencodeClient({
baseUrl: globalSDK.url,
signal: AbortSignal.timeout(1000 * 60 * 10),
fetch: platform.fetch,
directory: props.directory,
throwOnError: true,

View File

@@ -0,0 +1,185 @@
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { batch, createEffect, createMemo, createResource, createSignal, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { usePlatform } from "@/context/platform"
import { persisted } from "@/utils/persist"
type StoredProject = { worktree: string; expanded: boolean }
export function normalizeServerUrl(input: string) {
const trimmed = input.trim()
if (!trimmed) return
const withProtocol = /^https?:\/\//.test(trimmed) ? trimmed : `http://${trimmed}`
const cleaned = withProtocol.replace(/\/+$/, "")
return cleaned.replace(/^(https?:\/\/[^/]+).*/, "$1")
}
export function serverDisplayName(url: string) {
if (!url) return ""
return url
.replace(/^https?:\/\//, "")
.replace(/\/+$/, "")
.split("/")[0]
}
function projectsKey(url: string) {
if (!url) return ""
const host = url.replace(/^https?:\/\//, "").split(":")[0]
if (host === "localhost" || host === "127.0.0.1") return "local"
return url
}
export const { use: useServer, provider: ServerProvider } = createSimpleContext({
name: "Server",
init: (props: { defaultUrl: string }) => {
const platform = usePlatform()
const [store, setStore, _, ready] = persisted(
"server.v3",
createStore({
list: [] as string[],
projects: {} as Record<string, StoredProject[]>,
}),
)
const [active, setActiveRaw] = createSignal("")
function setActive(input: string) {
const url = normalizeServerUrl(input)
if (!url) return
setActiveRaw(url)
}
function add(input: string) {
const url = normalizeServerUrl(input)
if (!url) return
const fallback = normalizeServerUrl(props.defaultUrl)
if (fallback && url === fallback) {
setActiveRaw(url)
return
}
batch(() => {
if (!store.list.includes(url)) {
setStore("list", store.list.length, url)
}
setActiveRaw(url)
})
}
function remove(input: string) {
const url = normalizeServerUrl(input)
if (!url) return
const list = store.list.filter((x) => x !== url)
const next = active() === url ? (list[0] ?? normalizeServerUrl(props.defaultUrl) ?? "") : active()
batch(() => {
setStore("list", list)
setActiveRaw(next)
})
}
createEffect(() => {
if (!ready()) return
if (active()) return
const url = normalizeServerUrl(props.defaultUrl)
if (!url) return
setActiveRaw(url)
})
const isReady = createMemo(() => ready() && !!active())
const [healthy, { refetch }] = createResource(
() => active() || undefined,
async (url) => {
if (!url) return
const sdk = createOpencodeClient({
baseUrl: url,
fetch: platform.fetch,
signal: AbortSignal.timeout(3000),
})
return sdk.global
.health()
.then((x) => x.data?.healthy === true)
.catch(() => false)
},
)
createEffect(() => {
if (!active()) return
const interval = setInterval(() => refetch(), 10_000)
onCleanup(() => clearInterval(interval))
})
const origin = createMemo(() => projectsKey(active()))
const projectsList = createMemo(() => store.projects[origin()] ?? [])
const isLocal = createMemo(() => origin() === "local")
return {
ready: isReady,
healthy,
isLocal,
get url() {
return active()
},
get name() {
return serverDisplayName(active())
},
get list() {
return store.list
},
setActive,
add,
remove,
projects: {
list: projectsList,
open(directory: string) {
const key = origin()
if (!key) return
const current = store.projects[key] ?? []
if (current.find((x) => x.worktree === directory)) return
setStore("projects", key, [{ worktree: directory, expanded: true }, ...current])
},
close(directory: string) {
const key = origin()
if (!key) return
const current = store.projects[key] ?? []
setStore(
"projects",
key,
current.filter((x) => x.worktree !== directory),
)
},
expand(directory: string) {
const key = origin()
if (!key) return
const current = store.projects[key] ?? []
const index = current.findIndex((x) => x.worktree === directory)
if (index !== -1) setStore("projects", key, index, "expanded", true)
},
collapse(directory: string) {
const key = origin()
if (!key) return
const current = store.projects[key] ?? []
const index = current.findIndex((x) => x.worktree === directory)
if (index !== -1) setStore("projects", key, index, "expanded", false)
},
move(directory: string, toIndex: number) {
const key = origin()
if (!key) return
const current = store.projects[key] ?? []
const fromIndex = current.findIndex((x) => x.worktree === directory)
if (fromIndex === -1 || fromIndex === toIndex) return
const result = [...current]
const [item] = result.splice(fromIndex, 1)
result.splice(toIndex, 0, item)
setStore("projects", key, result)
},
},
}
},
})

View File

@@ -1,5 +1,5 @@
import { produce } from "solid-js/store"
import { createMemo } from "solid-js"
import { batch, createMemo } from "solid-js"
import { produce, reconcile } from "solid-js/store"
import { Binary } from "@opencode-ai/util/binary"
import { retry } from "@opencode-ai/util/retry"
import { createSimpleContext } from "@opencode-ai/ui/context"
@@ -18,8 +18,11 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
return {
data: store,
set: setStore,
get status() {
return store.status
},
get ready() {
return store.ready
return store.status !== "loading"
},
get project() {
const match = Binary.search(globalSync.data.project, store.project, (p) => p.id)
@@ -56,42 +59,75 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const result = Binary.search(messages, input.messageID, (m) => m.id)
messages.splice(result.index, 0, message)
}
draft.part[input.messageID] = input.parts.slice()
draft.part[input.messageID] = input.parts
.filter((p) => !!p?.id)
.slice()
.sort((a, b) => a.id.localeCompare(b.id))
}),
)
},
async sync(sessionID: string, _isRetry = false) {
const [session, messages, todo, diff] = await Promise.all([
retry(() => sdk.client.session.get({ sessionID })),
retry(() => sdk.client.session.messages({ sessionID, limit: 100 })),
retry(() => sdk.client.session.messages({ sessionID, limit: 1000 })),
retry(() => sdk.client.session.todo({ sessionID })),
retry(() => sdk.client.session.diff({ sessionID })),
])
setStore(
produce((draft) => {
const match = Binary.search(draft.session, sessionID, (s) => s.id)
if (match.found) draft.session[match.index] = session.data!
if (!match.found) draft.session.splice(match.index, 0, session.data!)
draft.todo[sessionID] = todo.data ?? []
draft.message[sessionID] = messages
.data!.map((x) => x.info)
.slice()
.sort((a, b) => a.id.localeCompare(b.id))
for (const message of messages.data!) {
draft.part[message.info.id] = message.parts.slice().sort((a, b) => a.id.localeCompare(b.id))
}
draft.session_diff[sessionID] = diff.data ?? []
}),
)
batch(() => {
setStore(
"session",
produce((draft) => {
const match = Binary.search(draft, sessionID, (s) => s.id)
if (match.found) {
draft[match.index] = session.data!
return
}
draft.splice(match.index, 0, session.data!)
}),
)
setStore("todo", sessionID, reconcile(todo.data ?? [], { key: "id" }))
setStore(
"message",
sessionID,
reconcile(
(messages.data ?? [])
.map((x) => x.info)
.filter((m) => !!m?.id)
.slice()
.sort((a, b) => a.id.localeCompare(b.id)),
{ key: "id" },
),
)
for (const message of messages.data ?? []) {
if (!message?.info?.id) continue
setStore(
"part",
message.info.id,
reconcile(
message.parts
.filter((p) => !!p?.id)
.slice()
.sort((a, b) => a.id.localeCompare(b.id)),
{ key: "id" },
),
)
}
setStore("session_diff", sessionID, reconcile(diff.data ?? [], { key: "file" }))
})
},
fetch: async (count = 10) => {
setStore("limit", (x) => x + count)
await sdk.client.session.list().then((x) => {
const sessions = (x.data ?? [])
.filter((s) => !!s?.id)
.slice()
.sort((a, b) => a.id.localeCompare(b.id))
.slice(0, store.limit)
setStore("session", sessions)
setStore("session", reconcile(sessions, { key: "id" }))
})
},
more: createMemo(() => store.session.length >= store.limit),

View File

@@ -36,35 +36,49 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
all: createMemo(() => Object.values(store.all)),
active: createMemo(() => store.active),
new() {
sdk.client.pty.create({ title: `Terminal ${store.all.length + 1}` }).then((pty) => {
const id = pty.data?.id
if (!id) return
setStore("all", [
...store.all,
{
id,
title: pty.data?.title ?? "Terminal",
},
])
setStore("active", id)
})
sdk.client.pty
.create({ title: `Terminal ${store.all.length + 1}` })
.then((pty) => {
const id = pty.data?.id
if (!id) return
setStore("all", [
...store.all,
{
id,
title: pty.data?.title ?? "Terminal",
},
])
setStore("active", id)
})
.catch((e) => {
console.error("Failed to create terminal", e)
})
},
update(pty: Partial<LocalPTY> & { id: string }) {
setStore("all", (x) => x.map((x) => (x.id === pty.id ? { ...x, ...pty } : x)))
sdk.client.pty.update({
ptyID: pty.id,
title: pty.title,
size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
})
sdk.client.pty
.update({
ptyID: pty.id,
title: pty.title,
size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
})
.catch((e) => {
console.error("Failed to update terminal", e)
})
},
async clone(id: string) {
const index = store.all.findIndex((x) => x.id === id)
const pty = store.all[index]
if (!pty) return
const clone = await sdk.client.pty.create({
title: pty.title,
})
if (!clone.data) return
const clone = await sdk.client.pty
.create({
title: pty.title,
})
.catch((e) => {
console.error("Failed to clone terminal", e)
return undefined
})
if (!clone?.data) return
setStore("all", index, {
...pty,
...clone.data,
@@ -88,7 +102,9 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
setStore("active", previous?.id)
}
})
await sdk.client.pty.remove({ ptyID: id })
await sdk.client.pty.remove({ ptyID: id }).catch((e) => {
console.error("Failed to close terminal", e)
})
},
move(id: string, to: number) {
const index = store.all.findIndex((f) => f.id === id)

View File

@@ -20,6 +20,36 @@ const platform: Platform = {
restart: async () => {
window.location.reload()
},
notify: async (title, description, href) => {
if (!("Notification" in window)) return
const permission =
Notification.permission === "default"
? await Notification.requestPermission().catch(() => "denied")
: Notification.permission
if (permission !== "granted") return
const inView = document.visibilityState === "visible" && document.hasFocus()
if (inView) return
await Promise.resolve()
.then(() => {
const notification = new Notification(title, {
body: description ?? "",
icon: "https://opencode.ai/favicon-96x96.png",
})
notification.onclick = () => {
window.focus()
if (href) {
window.history.pushState(null, "", href)
window.dispatchEvent(new PopStateEvent("popstate"))
}
notification.close()
}
})
.catch(() => undefined)
},
}
render(

View File

@@ -1,8 +1,9 @@
import { createMemo, Show, type ParentProps } from "solid-js"
import { useParams } from "@solidjs/router"
import { SDKProvider } from "@/context/sdk"
import { SDKProvider, useSDK } from "@/context/sdk"
import { SyncProvider, useSync } from "@/context/sync"
import { LocalProvider } from "@/context/local"
import { base64Decode } from "@opencode-ai/util/encode"
import { DataProvider } from "@opencode-ai/ui/context"
import { iife } from "@opencode-ai/util/iife"
@@ -18,8 +19,15 @@ export default function Layout(props: ParentProps) {
<SyncProvider>
{iife(() => {
const sync = useSync()
const sdk = useSDK()
const respond = (input: {
sessionID: string
permissionID: string
response: "once" | "always" | "reject"
}) => sdk.client.permission.respond(input)
return (
<DataProvider data={sync.data} directory={directory()}>
<DataProvider data={sync.data} directory={directory()} onPermissionRespond={respond}>
<LocalProvider>{props.children}</LocalProvider>
</DataProvider>
)

View File

@@ -2,6 +2,7 @@ import { TextField } from "@opencode-ai/ui/text-field"
import { Logo } from "@opencode-ai/ui/logo"
import { Button } from "@opencode-ai/ui/button"
import { Component, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { usePlatform } from "@/context/platform"
import { Icon } from "@opencode-ai/ui/icon"
@@ -20,11 +21,51 @@ function isInitError(error: unknown): error is InitError {
)
}
function safeJson(value: unknown): string {
const seen = new WeakSet<object>()
const json = JSON.stringify(
value,
(_key, val) => {
if (typeof val === "bigint") return val.toString()
if (typeof val === "object" && val) {
if (seen.has(val)) return "[Circular]"
seen.add(val)
}
return val
},
2,
)
return json ?? String(value)
}
function formatInitError(error: InitError): string {
const data = error.data
switch (error.name) {
case "MCPFailed":
return `MCP server "${data.name}" failed. Note, opencode does not support MCP authentication yet.`
case "ProviderAuthError": {
const providerID = typeof data.providerID === "string" ? data.providerID : "unknown"
const message = typeof data.message === "string" ? data.message : safeJson(data.message)
return `Provider authentication failed (${providerID}): ${message}`
}
case "APIError": {
const message = typeof data.message === "string" ? data.message : "API error"
const lines: string[] = [message]
if (typeof data.statusCode === "number") {
lines.push(`Status: ${data.statusCode}`)
}
if (typeof data.isRetryable === "boolean") {
lines.push(`Retryable: ${data.isRetryable}`)
}
if (typeof data.responseBody === "string" && data.responseBody) {
lines.push(`Response body:\n${data.responseBody}`)
}
return lines.join("\n")
}
case "ProviderModelNotFoundError": {
const { providerID, modelID, suggestions } = data as {
providerID: string
@@ -37,10 +78,14 @@ function formatInitError(error: InitError): string {
`Check your config (opencode.json) provider/model names`,
].join("\n")
}
case "ProviderInitError":
return `Failed to initialize provider "${data.providerID}". Check credentials and configuration.`
case "ConfigJsonError":
return `Config file at ${data.path} is not valid JSON(C)` + (data.message ? `: ${data.message}` : "")
case "ProviderInitError": {
const providerID = typeof data.providerID === "string" ? data.providerID : "unknown"
return `Failed to initialize provider "${providerID}". Check credentials and configuration.`
}
case "ConfigJsonError": {
const message = typeof data.message === "string" ? data.message : ""
return `Config file at ${data.path} is not valid JSON(C)` + (message ? `: ${message}` : "")
}
case "ConfigDirectoryTypoError":
return `Directory "${data.dir}" in ${data.path} is not valid. Rename the directory to "${data.suggestion}" or remove it. This is a common typo.`
case "ConfigFrontmatterError":
@@ -51,14 +96,14 @@ function formatInitError(error: InitError): string {
(issue: { message: string; path: string[] }) => "↳ " + issue.message + " " + issue.path.join("."),
)
: []
return [`Config file at ${data.path} is invalid` + (data.message ? `: ${data.message}` : ""), ...issues].join(
"\n",
)
const message = typeof data.message === "string" ? data.message : ""
return [`Config file at ${data.path} is invalid` + (message ? `: ${message}` : ""), ...issues].join("\n")
}
case "UnknownError":
return String(data.message)
return typeof data.message === "string" ? data.message : safeJson(data)
default:
return data.message ? String(data.message) : JSON.stringify(data, null, 2)
if (typeof data.message === "string") return data.message
return safeJson(data)
}
}
@@ -69,7 +114,7 @@ function formatErrorChain(error: unknown, depth = 0, parentMessage?: string): st
const message = formatInitError(error)
if (depth > 0 && parentMessage === message) return ""
const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
return indent + message
return indent + `${error.name}\n${message}`
}
if (error instanceof Error) {
@@ -77,15 +122,34 @@ function formatErrorChain(error: unknown, depth = 0, parentMessage?: string): st
const parts: string[] = []
const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
if (!isDuplicate) {
// Stack already includes error name and message, so prefer it
parts.push(indent + (error.stack ?? `${error.name}: ${error.message}`))
} else if (error.stack) {
// Duplicate message - only show the stack trace lines (skip message)
const trace = error.stack.split("\n").slice(1).join("\n").trim()
if (trace) {
parts.push(trace)
const header = `${error.name}${error.message ? `: ${error.message}` : ""}`
const stack = error.stack?.trim()
if (stack) {
const startsWithHeader = stack.startsWith(header)
if (isDuplicate && startsWithHeader) {
const trace = stack.split("\n").slice(1).join("\n").trim()
if (trace) {
parts.push(indent + trace)
}
}
if (isDuplicate && !startsWithHeader) {
parts.push(indent + stack)
}
if (!isDuplicate && startsWithHeader) {
parts.push(indent + stack)
}
if (!isDuplicate && !startsWithHeader) {
parts.push(indent + `${header}\n${stack}`)
}
}
if (!stack && !isDuplicate) {
parts.push(indent + header)
}
if (error.cause) {
@@ -105,7 +169,7 @@ function formatErrorChain(error: unknown, depth = 0, parentMessage?: string): st
}
const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
return indent + JSON.stringify(error, null, 2)
return indent + safeJson(error)
}
function formatError(error: unknown): string {
@@ -118,6 +182,25 @@ interface ErrorPageProps {
export const ErrorPage: Component<ErrorPageProps> = (props) => {
const platform = usePlatform()
const [store, setStore] = createStore({
checking: false,
version: undefined as string | undefined,
})
async function checkForUpdates() {
if (!platform.checkUpdate) return
setStore("checking", true)
const result = await platform.checkUpdate()
setStore("checking", false)
if (result.updateAvailable && result.version) setStore("version", result.version)
}
async function installUpdate() {
if (!platform.update || !platform.restart) return
await platform.update()
await platform.restart()
}
return (
<div class="relative flex-1 h-screen w-screen min-h-0 flex flex-col items-center justify-center bg-background-base font-sans">
<div class="w-2/3 max-w-3xl flex flex-col items-center justify-center gap-8">
@@ -131,13 +214,29 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
readOnly
copyable
multiline
class="max-h-96 w-full font-mono text-xs no-scrollbar whitespace-pre"
class="max-h-96 w-full font-mono text-xs no-scrollbar"
label="Error Details"
hideLabel
/>
<Button size="large" onClick={platform.restart}>
Restart
</Button>
<div class="flex items-center gap-3">
<Button size="large" onClick={platform.restart}>
Restart
</Button>
<Show when={platform.checkUpdate}>
<Show
when={store.version}
fallback={
<Button size="large" variant="ghost" onClick={checkForUpdates} disabled={store.checking}>
{store.checking ? "Checking..." : "Check for updates"}
</Button>
}
>
<Button size="large" onClick={installUpdate}>
Update to {store.version}
</Button>
</Show>
</Show>
</div>
<div class="flex flex-col items-center gap-2">
<div class="flex items-center justify-center gap-1">
Please report this error to the OpenCode team

View File

@@ -8,12 +8,18 @@ import { base64Encode } from "@opencode-ai/util/encode"
import { Icon } from "@opencode-ai/ui/icon"
import { usePlatform } from "@/context/platform"
import { DateTime } from "luxon"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
import { DialogSelectServer } from "@/components/dialog-select-server"
import { useServer } from "@/context/server"
export default function Home() {
const sync = useGlobalSync()
const layout = useLayout()
const platform = usePlatform()
const dialog = useDialog()
const navigate = useNavigate()
const server = useServer()
const homedir = createMemo(() => sync.data.path.home)
function openProject(directory: string) {
@@ -22,32 +28,57 @@ export default function Home() {
}
async function chooseProject() {
const result = await platform.openDirectoryPickerDialog?.({
title: "Open project",
multiple: true,
})
if (Array.isArray(result)) {
for (const directory of result) {
openProject(directory)
function resolve(result: string | string[] | null) {
if (Array.isArray(result)) {
for (const directory of result) {
openProject(directory)
}
} else if (result) {
openProject(result)
}
} else if (result) {
openProject(result)
}
if (platform.openDirectoryPickerDialog && server.isLocal()) {
const result = await platform.openDirectoryPickerDialog?.({
title: "Open project",
multiple: true,
})
resolve(result)
} else {
dialog.show(
() => <DialogSelectDirectory multiple={true} onSelect={resolve} />,
() => resolve(null),
)
}
}
return (
<div class="mx-auto mt-55">
<Logo class="w-xl opacity-12" />
<Button
size="large"
variant="ghost"
class="mt-4 mx-auto text-14-regular text-text-weak"
onClick={() => dialog.show(() => <DialogSelectServer />)}
>
<div
classList={{
"size-2 rounded-full": true,
"bg-icon-success-base": server.healthy() === true,
"bg-icon-critical-base": server.healthy() === false,
"bg-border-weak-base": server.healthy() === undefined,
}}
/>
{server.name}
</Button>
<Switch>
<Match when={sync.data.project.length > 0}>
<div class="mt-20 w-full flex flex-col gap-4">
<div class="flex gap-2 items-center justify-between pl-3">
<div class="text-14-medium text-text-strong">Recent projects</div>
<Show when={platform.openDirectoryPickerDialog}>
<Button icon="folder-add-left" size="normal" class="pl-2 pr-3" onClick={chooseProject}>
Open project
</Button>
</Show>
<Button icon="folder-add-left" size="normal" class="pl-2 pr-3" onClick={chooseProject}>
Open project
</Button>
</div>
<ul class="flex flex-col gap-2">
<For
@@ -80,11 +111,9 @@ export default function Home() {
<div class="text-12-regular text-text-weak">Get started by opening a local project</div>
</div>
<div />
<Show when={platform.openDirectoryPickerDialog}>
<Button class="px-3" onClick={chooseProject}>
Open project
</Button>
</Show>
<Button class="px-3" onClick={chooseProject}>
Open project
</Button>
</div>
</Match>
</Switch>

View File

@@ -22,10 +22,11 @@ import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { Collapsible } from "@opencode-ai/ui/collapsible"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { Spinner } from "@opencode-ai/ui/spinner"
import { Mark } from "@opencode-ai/ui/logo"
import { getFilename } from "@opencode-ai/util/path"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Session } from "@opencode-ai/sdk/v2/client"
@@ -41,31 +42,29 @@ import {
} from "@thisbeyond/solid-dnd"
import type { DragEvent } from "@thisbeyond/solid-dnd"
import { useProviders } from "@/hooks/use-providers"
import { showToast, Toast } from "@opencode-ai/ui/toast"
import { showToast, Toast, toaster } from "@opencode-ai/ui/toast"
import { useGlobalSDK } from "@/context/global-sdk"
import { useNotification } from "@/context/notification"
import { usePermission } from "@/context/permission"
import { Binary } from "@opencode-ai/util/binary"
import { Header } from "@/components/header"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
import { DialogSelectProvider } from "@/components/dialog-select-provider"
import { useCommand } from "@/context/command"
import { DialogEditProject } from "@/components/dialog-edit-project"
import { DialogSelectServer } from "@/components/dialog-select-server"
import { useCommand, type CommandOption } from "@/context/command"
import { ConstrainDragXAxis } from "@/utils/solid-dnd"
import { DialogSelectDirectory } from "@/components/dialog-select-directory"
import { useServer } from "@/context/server"
export default function Layout(props: ParentProps) {
const [store, setStore] = createStore({
lastSession: {} as { [directory: string]: string },
activeDraggable: undefined as string | undefined,
mobileSidebarOpen: false,
mobileProjectsExpanded: {} as Record<string, boolean>,
})
const mobileSidebar = {
open: () => store.mobileSidebarOpen,
show: () => setStore("mobileSidebarOpen", true),
hide: () => setStore("mobileSidebarOpen", false),
toggle: () => setStore("mobileSidebarOpen", (x) => !x),
}
const mobileProjects = {
expanded: (directory: string) => store.mobileProjectsExpanded[directory] ?? true,
expand: (directory: string) => setStore("mobileProjectsExpanded", directory, true),
@@ -84,17 +83,58 @@ export default function Layout(props: ParentProps) {
const globalSync = useGlobalSync()
const layout = useLayout()
const platform = usePlatform()
const server = useServer()
const notification = useNotification()
const permission = usePermission()
const navigate = useNavigate()
const providers = useProviders()
const dialog = useDialog()
const command = useCommand()
const theme = useTheme()
const availableThemeEntries = createMemo(() => Object.entries(theme.themes()))
const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"]
const colorSchemeLabel: Record<ColorScheme, string> = {
system: "System",
light: "Light",
dark: "Dark",
}
onMount(async () => {
if (platform.checkUpdate && platform.update && platform.restart) {
const { updateAvailable, version } = await platform.checkUpdate()
if (updateAvailable) {
showToast({
function cycleTheme(direction = 1) {
const ids = availableThemeEntries().map(([id]) => id)
if (ids.length === 0) return
const currentIndex = ids.indexOf(theme.themeId())
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + direction + ids.length) % ids.length
const nextThemeId = ids[nextIndex]
theme.setTheme(nextThemeId)
const nextTheme = theme.themes()[nextThemeId]
showToast({
title: "Theme switched",
description: nextTheme?.name ?? nextThemeId,
})
}
function cycleColorScheme(direction = 1) {
const current = theme.colorScheme()
const currentIndex = colorSchemeOrder.indexOf(current)
const nextIndex =
currentIndex === -1 ? 0 : (currentIndex + direction + colorSchemeOrder.length) % colorSchemeOrder.length
const next = colorSchemeOrder[nextIndex]
theme.setColorScheme(next)
showToast({
title: "Color scheme",
description: colorSchemeLabel[next],
})
}
onMount(() => {
if (!platform.checkUpdate || !platform.update || !platform.restart) return
let toastId: number | undefined
async function pollUpdate() {
const { updateAvailable, version } = await platform.checkUpdate!()
if (updateAvailable && toastId === undefined) {
toastId = showToast({
persistent: true,
icon: "download",
title: "Update available",
@@ -115,6 +155,94 @@ export default function Layout(props: ParentProps) {
})
}
}
pollUpdate()
const interval = setInterval(pollUpdate, 10 * 60 * 1000)
onCleanup(() => clearInterval(interval))
})
onMount(() => {
const toastBySession = new Map<string, number>()
const alertedAtBySession = new Map<string, number>()
const permissionAlertCooldownMs = 5000
const unsub = globalSDK.event.listen((e) => {
if (e.details?.type !== "permission.asked") return
const directory = e.name
const perm = e.details.properties
if (permission.autoResponds(perm)) return
const sessionKey = `${directory}:${perm.sessionID}`
const [store] = globalSync.child(directory)
const session = store.session.find((s) => s.id === perm.sessionID)
const sessionTitle = session?.title ?? "New session"
const projectName = getFilename(directory)
const description = `${sessionTitle} in ${projectName} needs permission`
const href = `/${base64Encode(directory)}/session/${perm.sessionID}`
const now = Date.now()
const lastAlerted = alertedAtBySession.get(sessionKey) ?? 0
if (now - lastAlerted < permissionAlertCooldownMs) return
alertedAtBySession.set(sessionKey, now)
void platform.notify("Permission required", description, href)
const currentDir = params.dir ? base64Decode(params.dir) : undefined
const currentSession = params.id
if (directory === currentDir && perm.sessionID === currentSession) return
if (directory === currentDir && session?.parentID === currentSession) return
const existingToastId = toastBySession.get(sessionKey)
if (existingToastId !== undefined) {
toaster.dismiss(existingToastId)
}
const toastId = showToast({
persistent: true,
icon: "checklist",
title: "Permission required",
description,
actions: [
{
label: "Go to session",
onClick: () => {
navigate(href)
},
},
{
label: "Dismiss",
onClick: "dismiss",
},
],
})
toastBySession.set(sessionKey, toastId)
})
onCleanup(unsub)
createEffect(() => {
const currentDir = params.dir ? base64Decode(params.dir) : undefined
const currentSession = params.id
if (!currentDir || !currentSession) return
const sessionKey = `${currentDir}:${currentSession}`
const toastId = toastBySession.get(sessionKey)
if (toastId !== undefined) {
toaster.dismiss(toastId)
toastBySession.delete(sessionKey)
alertedAtBySession.delete(sessionKey)
}
const [store] = globalSync.child(currentDir)
const childSessions = store.session.filter((s) => s.parentID === currentSession)
for (const child of childSessions) {
const childKey = `${currentDir}:${child.id}`
const childToastId = toastBySession.get(childKey)
if (childToastId !== undefined) {
toaster.dismiss(childToastId)
toastBySession.delete(childKey)
alertedAtBySession.delete(childKey)
}
}
})
})
function sortSessions(a: Session, b: Session) {
@@ -221,73 +349,124 @@ export default function Layout(props: ParentProps) {
}
}
command.register(() => [
{
id: "sidebar.toggle",
title: "Toggle sidebar",
category: "View",
keybind: "mod+b",
onSelect: () => layout.sidebar.toggle(),
},
...(platform.openDirectoryPickerDialog
? [
{
id: "project.open",
title: "Open project",
category: "Project",
keybind: "mod+o",
onSelect: () => chooseProject(),
},
]
: []),
{
id: "provider.connect",
title: "Connect provider",
category: "Provider",
onSelect: () => connectProvider(),
},
{
id: "session.previous",
title: "Previous session",
category: "Session",
keybind: "alt+arrowup",
onSelect: () => navigateSessionByOffset(-1),
},
{
id: "session.next",
title: "Next session",
category: "Session",
keybind: "alt+arrowdown",
onSelect: () => navigateSessionByOffset(1),
},
{
id: "session.archive",
title: "Archive session",
category: "Session",
keybind: "mod+shift+backspace",
disabled: !params.dir || !params.id,
onSelect: () => {
const session = currentSessions().find((s) => s.id === params.id)
if (session) archiveSession(session)
command.register(() => {
const commands: CommandOption[] = [
{
id: "sidebar.toggle",
title: "Toggle sidebar",
category: "View",
keybind: "mod+b",
onSelect: () => layout.sidebar.toggle(),
},
},
])
{
id: "project.open",
title: "Open project",
category: "Project",
keybind: "mod+o",
onSelect: () => chooseProject(),
},
{
id: "provider.connect",
title: "Connect provider",
category: "Provider",
onSelect: () => connectProvider(),
},
{
id: "server.switch",
title: "Switch server",
category: "Server",
onSelect: () => openServer(),
},
{
id: "session.previous",
title: "Previous session",
category: "Session",
keybind: "alt+arrowup",
onSelect: () => navigateSessionByOffset(-1),
},
{
id: "session.next",
title: "Next session",
category: "Session",
keybind: "alt+arrowdown",
onSelect: () => navigateSessionByOffset(1),
},
{
id: "session.archive",
title: "Archive session",
category: "Session",
keybind: "mod+shift+backspace",
disabled: !params.dir || !params.id,
onSelect: () => {
const session = currentSessions().find((s) => s.id === params.id)
if (session) archiveSession(session)
},
},
{
id: "theme.cycle",
title: "Cycle theme",
category: "Theme",
keybind: "mod+shift+t",
onSelect: () => cycleTheme(1),
},
]
for (const [id, definition] of availableThemeEntries()) {
commands.push({
id: `theme.set.${id}`,
title: `Use theme: ${definition.name ?? id}`,
category: "Theme",
onSelect: () => theme.commitPreview(),
onHighlight: () => {
theme.previewTheme(id)
return () => theme.cancelPreview()
},
})
}
commands.push({
id: "theme.scheme.cycle",
title: "Cycle color scheme",
category: "Theme",
keybind: "mod+shift+s",
onSelect: () => cycleColorScheme(1),
})
for (const scheme of colorSchemeOrder) {
commands.push({
id: `theme.scheme.${scheme}`,
title: `Use color scheme: ${colorSchemeLabel[scheme]}`,
category: "Theme",
onSelect: () => theme.commitPreview(),
onHighlight: () => {
theme.previewColorScheme(scheme)
return () => theme.cancelPreview()
},
})
}
return commands
})
function connectProvider() {
dialog.show(() => <DialogSelectProvider />)
}
function openServer() {
dialog.show(() => <DialogSelectServer />)
}
function navigateToProject(directory: string | undefined) {
if (!directory) return
const lastSession = store.lastSession[directory]
navigate(`/${base64Encode(directory)}${lastSession ? `/session/${lastSession}` : ""}`)
mobileSidebar.hide()
layout.mobileSidebar.hide()
}
function navigateToSession(session: Session | undefined) {
if (!session) return
navigate(`/${params.dir}/session/${session?.id}`)
mobileSidebar.hide()
layout.mobileSidebar.hide()
}
function openProject(directory: string, navigate = true) {
@@ -304,17 +483,28 @@ export default function Layout(props: ParentProps) {
}
async function chooseProject() {
const result = await platform.openDirectoryPickerDialog?.({
title: "Open project",
multiple: true,
})
if (Array.isArray(result)) {
for (const directory of result) {
openProject(directory, false)
function resolve(result: string | string[] | null) {
if (Array.isArray(result)) {
for (const directory of result) {
openProject(directory, false)
}
navigateToProject(result[0])
} else if (result) {
openProject(result)
}
navigateToProject(result[0])
} else if (result) {
openProject(result)
}
if (platform.openDirectoryPickerDialog && server.isLocal()) {
const result = await platform.openDirectoryPickerDialog?.({
title: "Open project",
multiple: true,
})
resolve(result)
} else {
dialog.show(
() => <DialogSelectDirectory multiple={true} onSelect={resolve} />,
() => resolve(null),
)
}
}
@@ -376,7 +566,7 @@ export default function Layout(props: ParentProps) {
const notification = useNotification()
const notifications = createMemo(() => notification.project.unseen(props.project.worktree))
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
const name = createMemo(() => getFilename(props.project.worktree))
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
const mask = "radial-gradient(circle 5px at calc(100% - 2px) 2px, transparent 5px, black 5.5px)"
const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
@@ -412,7 +602,7 @@ export default function Layout(props: ParentProps) {
}
const ProjectVisual = (props: { project: LocalProject; class?: string }): JSX.Element => {
const name = createMemo(() => getFilename(props.project.worktree))
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
const current = createMemo(() => base64Decode(params.dir ?? ""))
return (
<Switch>
@@ -454,8 +644,20 @@ export default function Layout(props: ParentProps) {
const updated = createMemo(() => DateTime.fromMillis(props.session.time.updated))
const notifications = createMemo(() => notification.session.unseen(props.session.id))
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
const hasPermissions = createMemo(() => {
const store = globalSync.child(props.project.worktree)[0]
const permissions = store.permission?.[props.session.id] ?? []
if (permissions.length > 0) return true
const childSessions = store.session.filter((s) => s.parentID === props.session.id)
for (const child of childSessions) {
const childPermissions = store.permission?.[child.id] ?? []
if (childPermissions.length > 0) return true
}
return false
})
const isWorking = createMemo(() => {
if (props.session.id === params.id) return false
if (hasPermissions()) return false
const status = globalSync.child(props.project.worktree)[0].session_status[props.session.id]
return status?.type === "busy" || status?.type === "retry"
})
@@ -486,6 +688,9 @@ export default function Layout(props: ParentProps) {
<Match when={isWorking()}>
<Spinner class="size-2.5 mr-0.5" />
</Match>
<Match when={hasPermissions()}>
<div class="size-1.5 mr-1.5 rounded-full bg-surface-warning-strong" />
</Match>
<Match when={hasError()}>
<div class="size-1.5 mr-1.5 rounded-full bg-text-diff-delete-base" />
</Match>
@@ -519,17 +724,13 @@ export default function Layout(props: ParentProps) {
</A>
</Tooltip>
<div class="hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute top-1 right-1">
<Tooltip
<TooltipKeybind
placement={props.mobile ? "bottom" : "right"}
value={
<div class="flex items-center gap-2">
<span>Archive session</span>
<span class="text-icon-base text-12-medium">{command.keybind("session.archive")}</span>
</div>
}
title="Archive session"
keybind={command.keybind("session.archive")}
>
<IconButton icon="archive" variant="ghost" onClick={() => archiveSession(props.session)} />
</Tooltip>
</TooltipKeybind>
</div>
</div>
</>
@@ -540,7 +741,7 @@ export default function Layout(props: ParentProps) {
const sortable = createSortable(props.project.worktree)
const showExpanded = createMemo(() => props.mobile || layout.sidebar.opened())
const slug = createMemo(() => base64Encode(props.project.worktree))
const name = createMemo(() => getFilename(props.project.worktree))
const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
const [store, setProjectStore] = globalSync.child(props.project.worktree)
const sessions = createMemo(() => store.session.toSorted(sortSessions))
const rootSessions = createMemo(() => sessions().filter((s) => !s.parentID))
@@ -586,23 +787,20 @@ export default function Layout(props: ParentProps) {
<DropdownMenu.Trigger as={IconButton} icon="dot-grid" variant="ghost" />
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item
onSelect={() => dialog.show(() => <DialogEditProject project={props.project} />)}
>
<DropdownMenu.ItemLabel>Edit project</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item onSelect={() => closeProject(props.project.worktree)}>
<DropdownMenu.ItemLabel>Close Project</DropdownMenu.ItemLabel>
<DropdownMenu.ItemLabel>Close project</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
<Tooltip
placement="top"
value={
<div class="flex items-center gap-2">
<span>New session</span>
<span class="text-icon-base text-12-medium">{command.keybind("session.new")}</span>
</div>
}
>
<TooltipKeybind placement="top" title="New session" keybind={command.keybind("session.new")}>
<IconButton as={A} href={`${slug()}/session`} icon="plus-small" variant="ghost" />
</Tooltip>
</TooltipKeybind>
</div>
</Button>
<Collapsible.Content>
@@ -680,15 +878,16 @@ export default function Layout(props: ParentProps) {
<>
<div class="flex flex-col items-start self-stretch gap-4 p-2 min-h-0 overflow-hidden">
<Show when={!sidebarProps.mobile}>
<Tooltip
<A href="/" class="shrink-0 h-8 flex items-center justify-start px-2" data-tauri-drag-region>
<Mark class="shrink-0" />
</A>
</Show>
<Show when={!sidebarProps.mobile}>
<TooltipKeybind
class="shrink-0"
placement="right"
value={
<div class="flex items-center gap-2">
<span>Toggle sidebar</span>
<span class="text-icon-base text-12-medium">{command.keybind("sidebar.toggle")}</span>
</div>
}
title="Toggle sidebar"
keybind={command.keybind("sidebar.toggle")}
inactive={expanded()}
>
<Button
@@ -720,7 +919,7 @@ export default function Layout(props: ParentProps) {
</div>
</Show>
</Button>
</Tooltip>
</TooltipKeybind>
</Show>
<DragDropProvider
onDragStart={handleDragStart}
@@ -749,7 +948,7 @@ export default function Layout(props: ParentProps) {
</div>
<div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
<Switch>
<Match when={!providers.paid().length && expanded()}>
<Match when={providers.all().length > 0 && !providers.paid().length && expanded()}>
<div class="rounded-md bg-background-stronger shadow-xs-border-base">
<div class="p-3 flex flex-col gap-2">
<div class="text-12-medium text-text-strong">Getting started</div>
@@ -768,7 +967,7 @@ export default function Layout(props: ParentProps) {
</Tooltip>
</div>
</Match>
<Match when={true}>
<Match when={providers.all().length > 0}>
<Tooltip placement="right" value="Connect provider" inactive={expanded()}>
<Button
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
@@ -782,30 +981,28 @@ export default function Layout(props: ParentProps) {
</Tooltip>
</Match>
</Switch>
<Show when={platform.openDirectoryPickerDialog}>
<Tooltip
placement="right"
value={
<div class="flex items-center gap-2">
<span>Open project</span>
<Show when={!sidebarProps.mobile}>
<span class="text-icon-base text-12-medium">{command.keybind("project.open")}</span>
</Show>
</div>
}
inactive={expanded()}
<Tooltip
placement="right"
value={
<div class="flex items-center gap-2">
<span>Open project</span>
<Show when={!sidebarProps.mobile}>
<span class="text-icon-base text-12-medium">{command.keybind("project.open")}</span>
</Show>
</div>
}
inactive={expanded()}
>
<Button
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
variant="ghost"
size="large"
icon="folder-add-left"
onClick={chooseProject}
>
<Button
class="flex w-full text-left justify-start text-text-base stroke-[1.5px] rounded-lg px-2"
variant="ghost"
size="large"
icon="folder-add-left"
onClick={chooseProject}
>
<Show when={expanded()}>Open project</Show>
</Button>
</Tooltip>
</Show>
<Show when={expanded()}>Open project</Show>
</Button>
</Tooltip>
<Tooltip placement="right" value="Share feedback" inactive={expanded()}>
<Button
as={"a"}
@@ -825,22 +1022,24 @@ export default function Layout(props: ParentProps) {
}
return (
<div class="relative flex-1 min-h-0 flex flex-col">
<Header
navigateToProject={navigateToProject}
navigateToSession={navigateToSession}
onMobileMenuToggle={mobileSidebar.toggle}
/>
<div class="relative flex-1 min-h-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
<div class="flex-1 min-h-0 flex">
<div
classList={{
"hidden xl:flex": true,
"relative @container w-12 pb-5 shrink-0 bg-background-base": true,
"flex-col gap-5.5 items-start self-stretch justify-between": true,
"border-r border-border-weak-base contain-strict": true,
"hidden xl:block": true,
"relative shrink-0": true,
}}
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : "48px" }}
>
<div
classList={{
"@container w-full h-full pb-5 bg-background-base": true,
"flex flex-col gap-5.5 items-start self-stretch justify-between": true,
"border-r border-border-weak-base contain-strict": true,
}}
>
<SidebarContent />
</div>
<Show when={layout.sidebar.opened()}>
<ResizeHandle
direction="horizontal"
@@ -852,24 +1051,23 @@ export default function Layout(props: ParentProps) {
onCollapse={layout.sidebar.close}
/>
</Show>
<SidebarContent />
</div>
<div class="xl:hidden">
<div
classList={{
"fixed inset-0 bg-black/50 z-40 transition-opacity duration-200": true,
"opacity-100 pointer-events-auto": mobileSidebar.open(),
"opacity-0 pointer-events-none": !mobileSidebar.open(),
"opacity-100 pointer-events-auto": layout.mobileSidebar.opened(),
"opacity-0 pointer-events-none": !layout.mobileSidebar.opened(),
}}
onClick={(e) => {
if (e.target === e.currentTarget) mobileSidebar.hide()
if (e.target === e.currentTarget) layout.mobileSidebar.hide()
}}
/>
<div
classList={{
"@container fixed inset-y-0 left-0 z-50 w-72 bg-background-base border-r border-border-weak-base flex flex-col gap-5.5 items-start self-stretch justify-between pt-12 pb-5 transition-transform duration-200 ease-out": true,
"translate-x-0": mobileSidebar.open(),
"-translate-x-full": !mobileSidebar.open(),
"translate-x-0": layout.mobileSidebar.opened(),
"-translate-x-full": !layout.mobileSidebar.opened(),
}}
onClick={(e) => e.stopPropagation()}
>

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,6 @@ export default defineConfig({
},
build: {
target: "esnext",
sourcemap: true,
// sourcemap: true,
},
})

View File

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

View File

@@ -7,10 +7,10 @@ export const config = {
// GitHub
github: {
repoUrl: "https://github.com/sst/opencode",
repoUrl: "https://github.com/anomalyco/opencode",
starsFormatted: {
compact: "41K",
full: "41,000",
compact: "45K",
full: "45,000",
},
},
@@ -22,8 +22,8 @@ export const config = {
// Static stats (used on landing page)
stats: {
contributors: "450",
commits: "6,000",
monthlyUsers: "400,000",
contributors: "500",
commits: "6,500",
monthlyUsers: "650,000",
},
} as const

View File

@@ -26,7 +26,7 @@ export default function NotFound() {
<a href="/docs">Docs</a>
</div>
<div data-slot="action">
<a href="https://github.com/sst/opencode">GitHub</a>
<a href="https://github.com/anomalyco/opencode">GitHub</a>
</div>
<div data-slot="action">
<a href="/discord">Discord</a>

View File

@@ -5,28 +5,36 @@ import { useAuthSession } from "~/context/auth.session"
export async function GET(input: APIEvent) {
const url = new URL(input.request.url)
const code = url.searchParams.get("code")
if (!code) throw new Error("No code found")
const result = await AuthClient.exchange(code, `${url.origin}${url.pathname}`)
if (result.err) {
throw new Error(result.err.message)
}
const decoded = AuthClient.decode(result.tokens.access, {} as any)
if (decoded.err) throw new Error(decoded.err.message)
const session = await useAuthSession()
const id = decoded.subject.properties.accountID
await session.update((value) => {
return {
...value,
account: {
...value.account,
[id]: {
id,
email: decoded.subject.properties.email,
try {
const code = url.searchParams.get("code")
if (!code) throw new Error("No code found")
const result = await AuthClient.exchange(code, `${url.origin}${url.pathname}`)
if (result.err) throw new Error(result.err.message)
const decoded = AuthClient.decode(result.tokens.access, {} as any)
if (decoded.err) throw new Error(decoded.err.message)
const session = await useAuthSession()
const id = decoded.subject.properties.accountID
await session.update((value) => {
return {
...value,
account: {
...value.account,
[id]: {
id,
email: decoded.subject.properties.email,
},
},
},
current: id,
}
})
return redirect("/auth")
current: id,
}
})
return redirect("/auth")
} catch (e: any) {
return new Response(
JSON.stringify({
error: e.message,
cause: Object.fromEntries(url.searchParams.entries()),
}),
{ status: 500 },
)
}
}

View File

@@ -0,0 +1,365 @@
import { Title } from "@solidjs/meta"
import { createAsync, query, useParams } from "@solidjs/router"
import { createSignal, For, Show } from "solid-js"
import { Database, desc, eq } from "@opencode-ai/console-core/drizzle/index.js"
import { BenchmarkTable } from "@opencode-ai/console-core/schema/benchmark.sql.js"
interface TaskSource {
repo: string
from: string
to: string
}
interface Judge {
score: number
rationale: string
judge: string
}
interface ScoreDetail {
criterion: string
weight: number
average: number
variance?: number
judges?: Judge[]
}
interface RunUsage {
input: number
output: number
cost: number
}
interface Run {
task: string
model: string
agent: string
score: {
final: number
base: number
penalty: number
}
scoreDetails: ScoreDetail[]
usage?: RunUsage
duration?: number
}
interface Prompt {
commit: string
prompt: string
}
interface AverageUsage {
input: number
output: number
cost: number
}
interface Task {
averageScore: number
averageDuration?: number
averageUsage?: AverageUsage
model?: string
agent?: string
summary?: string
runs?: Run[]
task: {
id: string
source: TaskSource
prompts?: Prompt[]
}
}
interface BenchmarkResult {
averageScore: number
tasks: Task[]
}
async function getTaskDetail(benchmarkId: string, taskId: string) {
"use server"
const rows = await Database.use((tx) =>
tx.select().from(BenchmarkTable).where(eq(BenchmarkTable.id, benchmarkId)).limit(1),
)
if (!rows[0]) return null
const parsed = JSON.parse(rows[0].result) as BenchmarkResult
const task = parsed.tasks.find((t) => t.task.id === taskId)
return task ?? null
}
const queryTaskDetail = query(getTaskDetail, "benchmark.task.detail")
function formatDuration(ms: number): string {
const seconds = Math.floor(ms / 1000)
const minutes = Math.floor(seconds / 60)
const remainingSeconds = seconds % 60
if (minutes > 0) {
return `${minutes}m ${remainingSeconds}s`
}
return `${remainingSeconds}s`
}
export default function BenchDetail() {
const params = useParams()
const [benchmarkId, taskId] = (params.id ?? "").split(":")
const task = createAsync(() => queryTaskDetail(benchmarkId, taskId))
return (
<main data-page="bench-detail">
<Title>Benchmark - {taskId}</Title>
<div style={{ padding: "1rem" }}>
<Show when={task()} fallback={<p>Task not found</p>}>
<div style={{ "margin-bottom": "1rem" }}>
<div>
<strong>Agent: </strong>
{task()?.agent ?? "N/A"}
</div>
<div>
<strong>Model: </strong>
{task()?.model ?? "N/A"}
</div>
<div>
<strong>Task: </strong>
{task()!.task.id}
</div>
</div>
<div style={{ "margin-bottom": "1rem" }}>
<div>
<strong>Repo: </strong>
<a
href={`https://github.com/${task()!.task.source.repo}`}
target="_blank"
rel="noopener noreferrer"
style={{ color: "#0066cc" }}
>
{task()!.task.source.repo}
</a>
</div>
<div>
<strong>From: </strong>
<a
href={`https://github.com/${task()!.task.source.repo}/commit/${task()!.task.source.from}`}
target="_blank"
rel="noopener noreferrer"
style={{ color: "#0066cc" }}
>
{task()!.task.source.from.slice(0, 7)}
</a>
</div>
<div>
<strong>To: </strong>
<a
href={`https://github.com/${task()!.task.source.repo}/commit/${task()!.task.source.to}`}
target="_blank"
rel="noopener noreferrer"
style={{ color: "#0066cc" }}
>
{task()!.task.source.to.slice(0, 7)}
</a>
</div>
</div>
<Show when={task()?.task.prompts && task()!.task.prompts!.length > 0}>
<div style={{ "margin-bottom": "1rem" }}>
<strong>Prompt:</strong>
<For each={task()!.task.prompts}>
{(p) => (
<div style={{ "margin-top": "0.5rem" }}>
<div style={{ "font-size": "0.875rem", color: "#666" }}>Commit: {p.commit.slice(0, 7)}</div>
<p style={{ "margin-top": "0.25rem", "white-space": "pre-wrap" }}>{p.prompt}</p>
</div>
)}
</For>
</div>
</Show>
<hr style={{ margin: "1rem 0", border: "none", "border-top": "1px solid #ccc" }} />
<div style={{ "margin-bottom": "1rem" }}>
<div>
<strong>Average Duration: </strong>
{task()?.averageDuration ? formatDuration(task()!.averageDuration!) : "N/A"}
</div>
<div>
<strong>Average Score: </strong>
{task()?.averageScore?.toFixed(3) ?? "N/A"}
</div>
<div>
<strong>Average Cost: </strong>
{task()?.averageUsage?.cost ? `$${task()!.averageUsage!.cost.toFixed(4)}` : "N/A"}
</div>
</div>
<Show when={task()?.summary}>
<div style={{ "margin-bottom": "1rem" }}>
<strong>Summary:</strong>
<p style={{ "margin-top": "0.5rem", "white-space": "pre-wrap" }}>{task()!.summary}</p>
</div>
</Show>
<Show when={task()?.runs && task()!.runs!.length > 0}>
<div style={{ "margin-bottom": "1rem" }}>
<strong>Runs:</strong>
<table style={{ "margin-top": "0.5rem", "border-collapse": "collapse", width: "100%" }}>
<thead>
<tr>
<th style={{ border: "1px solid #ccc", padding: "0.5rem", "text-align": "left" }}>Run</th>
<th
style={{
border: "1px solid #ccc",
padding: "0.5rem",
"text-align": "left",
"white-space": "nowrap",
}}
>
Score (Base - Penalty)
</th>
<th style={{ border: "1px solid #ccc", padding: "0.5rem", "text-align": "left" }}>Cost</th>
<th style={{ border: "1px solid #ccc", padding: "0.5rem", "text-align": "left" }}>Duration</th>
<For each={task()!.runs![0]?.scoreDetails}>
{(detail) => (
<th style={{ border: "1px solid #ccc", padding: "0.5rem", "text-align": "left" }}>
{detail.criterion} ({detail.weight})
</th>
)}
</For>
</tr>
</thead>
<tbody>
<For each={task()!.runs}>
{(run, index) => (
<tr>
<td style={{ border: "1px solid #ccc", padding: "0.5rem" }}>{index() + 1}</td>
<td style={{ border: "1px solid #ccc", padding: "0.5rem", "white-space": "nowrap" }}>
{run.score.final.toFixed(3)} ({run.score.base.toFixed(3)} - {run.score.penalty.toFixed(3)})
</td>
<td style={{ border: "1px solid #ccc", padding: "0.5rem" }}>
{run.usage?.cost ? `$${run.usage.cost.toFixed(4)}` : "N/A"}
</td>
<td style={{ border: "1px solid #ccc", padding: "0.5rem" }}>
{run.duration ? formatDuration(run.duration) : "N/A"}
</td>
<For each={run.scoreDetails}>
{(detail) => (
<td style={{ border: "1px solid #ccc", padding: "0.5rem" }}>
<For each={detail.judges}>
{(judge) => (
<span
style={{
color: judge.score === 1 ? "green" : judge.score === 0 ? "red" : "inherit",
"margin-right": "0.25rem",
}}
>
{judge.score === 1 ? "✓" : judge.score === 0 ? "✗" : judge.score}
</span>
)}
</For>
</td>
)}
</For>
</tr>
)}
</For>
</tbody>
</table>
<For each={task()!.runs}>
{(run, index) => (
<div style={{ "margin-top": "1rem" }}>
<h3 style={{ margin: "0 0 0.5rem 0" }}>Run {index() + 1}</h3>
<div>
<strong>Score: </strong>
{run.score.final.toFixed(3)} (Base: {run.score.base.toFixed(3)} - Penalty:{" "}
{run.score.penalty.toFixed(3)})
</div>
<For each={run.scoreDetails}>
{(detail) => (
<div style={{ "margin-top": "1rem", "padding-left": "1rem", "border-left": "2px solid #ccc" }}>
<div>
{detail.criterion} (weight: {detail.weight}){" "}
<For each={detail.judges}>
{(judge) => (
<span
style={{
color: judge.score === 1 ? "green" : judge.score === 0 ? "red" : "inherit",
"margin-right": "0.25rem",
}}
>
{judge.score === 1 ? "✓" : judge.score === 0 ? "✗" : judge.score}
</span>
)}
</For>
</div>
<Show when={detail.judges && detail.judges.length > 0}>
<For each={detail.judges}>
{(judge) => {
const [expanded, setExpanded] = createSignal(false)
return (
<div style={{ "margin-top": "0.5rem", "padding-left": "1rem" }}>
<div
style={{ "font-size": "0.875rem", cursor: "pointer" }}
onClick={() => setExpanded(!expanded())}
>
<span style={{ "margin-right": "0.5rem" }}>{expanded() ? "▼" : "▶"}</span>
<span
style={{
color: judge.score === 1 ? "green" : judge.score === 0 ? "red" : "inherit",
}}
>
{judge.score === 1 ? "✓" : judge.score === 0 ? "✗" : judge.score}
</span>{" "}
{judge.judge}
</div>
<Show when={expanded()}>
<p
style={{
margin: "0.25rem 0 0 0",
"white-space": "pre-wrap",
"font-size": "0.875rem",
}}
>
{judge.rationale}
</p>
</Show>
</div>
)
}}
</For>
</Show>
</div>
)}
</For>
</div>
)}
</For>
</div>
</Show>
{(() => {
const [jsonExpanded, setJsonExpanded] = createSignal(false)
return (
<div style={{ "margin-top": "1rem" }}>
<button
style={{
cursor: "pointer",
padding: "0.75rem 1.5rem",
"font-size": "1rem",
background: "#f0f0f0",
border: "1px solid #ccc",
"border-radius": "4px",
}}
onClick={() => setJsonExpanded(!jsonExpanded())}
>
<span style={{ "margin-right": "0.5rem" }}>{jsonExpanded() ? "▼" : "▶"}</span>
Raw JSON
</button>
<Show when={jsonExpanded()}>
<pre>{JSON.stringify(task(), null, 2)}</pre>
</Show>
</div>
)
})()}
</Show>
</div>
</main>
)
}

View File

@@ -0,0 +1,86 @@
import { Title } from "@solidjs/meta"
import { A, createAsync, query } from "@solidjs/router"
import { createMemo, For, Show } from "solid-js"
import { Database, desc } from "@opencode-ai/console-core/drizzle/index.js"
import { BenchmarkTable } from "@opencode-ai/console-core/schema/benchmark.sql.js"
interface BenchmarkResult {
averageScore: number
tasks: { averageScore: number; task: { id: string } }[]
}
async function getBenchmarks() {
"use server"
const rows = await Database.use((tx) =>
tx.select().from(BenchmarkTable).orderBy(desc(BenchmarkTable.timeCreated)).limit(100),
)
return rows.map((row) => {
const parsed = JSON.parse(row.result) as BenchmarkResult
const taskScores: Record<string, number> = {}
for (const t of parsed.tasks) {
taskScores[t.task.id] = t.averageScore
}
return {
id: row.id,
agent: row.agent,
model: row.model,
averageScore: parsed.averageScore,
taskScores,
}
})
}
const queryBenchmarks = query(getBenchmarks, "benchmarks.list")
export default function Bench() {
const benchmarks = createAsync(() => queryBenchmarks())
const taskIds = createMemo(() => {
const ids = new Set<string>()
for (const row of benchmarks() ?? []) {
for (const id of Object.keys(row.taskScores)) {
ids.add(id)
}
}
return [...ids].sort()
})
return (
<main data-page="bench" style={{ padding: "2rem" }}>
<Title>Benchmark</Title>
<h1 style={{ "margin-bottom": "1.5rem" }}>Benchmarks</h1>
<table style={{ "border-collapse": "collapse", width: "100%" }}>
<thead>
<tr>
<th style={{ "text-align": "left", padding: "0.75rem" }}>Agent</th>
<th style={{ "text-align": "left", padding: "0.75rem" }}>Model</th>
<th style={{ "text-align": "left", padding: "0.75rem" }}>Score</th>
<For each={taskIds()}>{(id) => <th style={{ "text-align": "left", padding: "0.75rem" }}>{id}</th>}</For>
</tr>
</thead>
<tbody>
<For each={benchmarks()}>
{(row) => (
<tr>
<td style={{ padding: "0.75rem" }}>{row.agent}</td>
<td style={{ padding: "0.75rem" }}>{row.model}</td>
<td style={{ padding: "0.75rem" }}>{row.averageScore.toFixed(3)}</td>
<For each={taskIds()}>
{(id) => (
<td style={{ padding: "0.75rem" }}>
<Show when={row.taskScores[id] !== undefined} fallback="">
<A href={`/bench/${row.id}:${id}`} style={{ color: "#0066cc" }}>
{row.taskScores[id]?.toFixed(3)}
</A>
</Show>
</td>
)}
</For>
</tr>
)}
</For>
</tbody>
</table>
</main>
)
}

View File

@@ -0,0 +1,29 @@
import type { APIEvent } from "@solidjs/start/server"
import { Database } from "@opencode-ai/console-core/drizzle/index.js"
import { BenchmarkTable } from "@opencode-ai/console-core/schema/benchmark.sql.js"
import { Identifier } from "@opencode-ai/console-core/identifier.js"
interface SubmissionBody {
model: string
agent: string
result: string
}
export async function POST(event: APIEvent) {
const body = (await event.request.json()) as SubmissionBody
if (!body.model || !body.agent || !body.result) {
return Response.json({ error: "All fields are required" }, { status: 400 })
}
await Database.use((tx) =>
tx.insert(BenchmarkTable).values({
id: Identifier.create("benchmark"),
model: body.model,
agent: body.agent,
result: body.result,
}),
)
return Response.json({ success: true }, { status: 200 })
}

View File

@@ -21,7 +21,7 @@ export async function GET({ params: { platform } }: APIEvent) {
const assetName = assetNames[platform]
if (!assetName) return new Response("Not Found", { status: 404 })
const resp = await fetch(`https://github.com/sst/opencode/releases/latest/download/${assetName}`, {
const resp = await fetch(`https://github.com/anomalyco/opencode/releases/latest/download/${assetName}`, {
cf: {
// in case gh releases has rate limits
cacheTtl: 60 * 60 * 24,

View File

@@ -1,6 +1,6 @@
export async function GET() {
const response = await fetch(
"https://raw.githubusercontent.com/sst/opencode/refs/heads/dev/packages/sdk/openapi.json",
"https://raw.githubusercontent.com/anomalyco/opencode/refs/heads/dev/packages/sdk/openapi.json",
)
const json = await response.json()
return json

View File

@@ -151,7 +151,7 @@ export default function Home() {
<a href="https://x.com/opencode">X.com</a>
</div>
<div data-slot="cell">
<a href="https://github.com/sst/opencode">GitHub</a>
<a href="https://github.com/anomalyco/opencode">GitHub</a>
</div>
<div data-slot="cell">
<a href="https://opencode.ai/discord">Discord</a>

View File

@@ -1,5 +1,5 @@
import { json, action, useParams, createAsync, useSubmission } from "@solidjs/router"
import { createEffect, Show } from "solid-js"
import { createEffect, Show, createMemo } from "solid-js"
import { createStore } from "solid-js/store"
import { withActor } from "~/context/auth.withActor"
import { Billing } from "@opencode-ai/console-core/billing.js"
@@ -68,6 +68,12 @@ export function ReloadSection() {
reloadTrigger: "",
})
const processingFee = createMemo(() => {
const reloadAmount = billingInfo()?.reloadAmount
if (!reloadAmount) return "0.00"
return (((reloadAmount + 0.3) / 0.956) * 0.044 + 0.3).toFixed(2)
})
createEffect(() => {
if (!setReloadSubmission.pending && setReloadSubmission.result && !(setReloadSubmission.result as any).error) {
setStore("show", false)
@@ -104,8 +110,8 @@ export function ReloadSection() {
}
>
<p>
Auto reload is <b>enabled</b>. We'll reload <b>${billingInfo()?.reloadAmount}</b> (+$1.23 processing fee)
when balance reaches <b>${billingInfo()?.reloadTrigger}</b>.
Auto reload is <b>enabled</b>. We'll reload <b>${billingInfo()?.reloadAmount}</b> (+${processingFee()}{" "}
processing fee) when balance reaches <b>${billingInfo()?.reloadTrigger}</b>.
</p>
</Show>
<button data-color="primary" type="button" onClick={() => show()}>

View File

@@ -124,6 +124,8 @@ export async function handler(
res.status !== 200 &&
// ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found.
res.status !== 404 &&
// ie. cannot change codex model providers mid-session
!modelInfo.stickyProvider &&
modelInfo.fallbackProvider &&
providerInfo.id !== modelInfo.fallbackProvider
) {

View File

@@ -0,0 +1,12 @@
CREATE TABLE `benchmark` (
`id` varchar(30) NOT NULL,
`time_created` timestamp(3) NOT NULL DEFAULT (now()),
`time_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`time_deleted` timestamp(3),
`model` varchar(64) NOT NULL,
`agent` varchar(64) NOT NULL,
`result` mediumtext NOT NULL,
CONSTRAINT `benchmark_id_pk` PRIMARY KEY(`id`)
);
--> statement-breakpoint
CREATE INDEX `time_created` ON `benchmark` (`time_created`);

File diff suppressed because it is too large Load Diff

View File

@@ -274,6 +274,13 @@
"when": 1764110043942,
"tag": "0038_famous_magik",
"breakpoints": true
},
{
"idx": 39,
"version": "5",
"when": 1766946179892,
"tag": "0039_striped_forge",
"breakpoints": true
}
]
}

View File

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

View File

@@ -5,6 +5,7 @@ export namespace Identifier {
const prefixes = {
account: "acc",
auth: "aut",
benchmark: "ben",
billing: "bil",
key: "key",
model: "mod",

View File

@@ -0,0 +1,14 @@
import { index, mediumtext, mysqlTable, primaryKey, varchar } from "drizzle-orm/mysql-core"
import { id, timestamps } from "../drizzle/types"
export const BenchmarkTable = mysqlTable(
"benchmark",
{
id: id(),
...timestamps,
model: varchar("model", { length: 64 }).notNull(),
agent: varchar("agent", { length: 64 }).notNull(),
result: mediumtext("result").notNull(),
},
(table) => [primaryKey({ columns: [table.id] }), index("time_created").on(table.timeCreated)],
)

View File

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

View File

@@ -123,7 +123,11 @@ export default {
},
}).then((x) => x.json())) as any
subject = user.id.toString()
email = emails.find((x: any) => x.primary && x.verified)?.email
const primaryEmail = emails.find((x: any) => x.primary)
if (!primaryEmail) throw new Error("No primary email found for GitHub user")
if (!primaryEmail.verified) throw new Error("Primary email for GitHub user not verified")
email = primaryEmail.email
} else if (response.provider === "google") {
if (!response.id.email_verified) throw new Error("Google email not verified")
subject = response.id.sub as string

View File

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

View File

@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="en" style="background-color: var(--background-base)">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -13,14 +13,38 @@
<meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
<meta property="og:image" content="/social-share.png" />
<meta property="twitter:image" content="/social-share.png" />
</head>
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
<script>
<script id="oc-theme-preload-script">
;(function () {
const savedTheme = localStorage.getItem("theme") || "oc-1"
document.documentElement.setAttribute("data-theme", savedTheme)
var themeId = localStorage.getItem("opencode-theme-id")
if (!themeId) return
var scheme = localStorage.getItem("opencode-color-scheme") || "system"
var isDark = scheme === "dark" || (scheme === "system" && matchMedia("(prefers-color-scheme: dark)").matches)
var mode = isDark ? "dark" : "light"
document.documentElement.dataset.theme = themeId
document.documentElement.dataset.colorScheme = mode
if (themeId === "oc-1") return
var css = localStorage.getItem("opencode-theme-css-" + themeId + "-" + mode)
if (css) {
var style = document.createElement("style")
style.id = "oc-theme-preload"
style.textContent =
":root{color-scheme:" +
mode +
";--text-mix-blend-mode:" +
(isDark ? "plus-lighter" : "multiply") +
";" +
css +
"}"
document.head.appendChild(style)
}
})()
</script>
</head>
<body class="antialiased overscroll-none text-12-regular overflow-hidden">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root" class="flex flex-col h-screen"></div>
<script src="/src/index.tsx" type="module"></script>

View File

@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.0.203",
"version": "1.0.224",
"type": "module",
"scripts": {
"typecheck": "tsgo -b",
@@ -18,6 +18,7 @@
"@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-os": "~2",
"@tauri-apps/plugin-notification": "~2",
"@tauri-apps/plugin-process": "~2",
"@tauri-apps/plugin-shell": "~2",
"@tauri-apps/plugin-store": "~2",

View File

@@ -6,7 +6,7 @@ const RUST_TARGET = Bun.env.TAURI_ENV_TARGET_TRIPLE
const sidecarConfig = getCurrentSidecar(RUST_TARGET)
const binaryPath = `../opencode/dist/${sidecarConfig.ocBinary}/bin/opencode`
const binaryPath = `../opencode/dist/${sidecarConfig.ocBinary}/bin/opencode${process.platform === "win32" ? ".exe" : ""}`
await $`cd ../opencode && bun run build --single`

View File

@@ -2210,6 +2210,18 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "mac-notification-sys"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65fd3f75411f4725061682ed91f131946e912859d0044d39c4ec0aac818d7621"
dependencies = [
"cc",
"objc2 0.6.3",
"objc2-foundation 0.3.2",
"time",
]
[[package]]
name = "markup5ever"
version = "0.14.1"
@@ -2384,6 +2396,20 @@ dependencies = [
"memchr",
]
[[package]]
name = "notify-rust"
version = "4.11.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6442248665a5aa2514e794af3b39661a8e73033b1cc5e59899e1276117ee4400"
dependencies = [
"futures-lite",
"log",
"mac-notification-sys",
"serde",
"tauri-winrt-notification",
"zbus",
]
[[package]]
name = "num-conv"
version = "0.1.0"
@@ -2758,6 +2784,7 @@ dependencies = [
"tauri-plugin-clipboard-manager",
"tauri-plugin-dialog",
"tauri-plugin-http",
"tauri-plugin-notification",
"tauri-plugin-opener",
"tauri-plugin-os",
"tauri-plugin-process",
@@ -4519,6 +4546,25 @@ dependencies = [
"urlpattern",
]
[[package]]
name = "tauri-plugin-notification"
version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc"
dependencies = [
"log",
"notify-rust",
"rand 0.9.2",
"serde",
"serde_json",
"serde_repr",
"tauri",
"tauri-plugin",
"thiserror 2.0.17",
"time",
"url",
]
[[package]]
name = "tauri-plugin-opener"
version = "2.5.2"
@@ -4754,6 +4800,18 @@ dependencies = [
"toml 0.9.8",
]
[[package]]
name = "tauri-winrt-notification"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9"
dependencies = [
"quick-xml 0.37.5",
"thiserror 2.0.17",
"windows",
"windows-version",
]
[[package]]
name = "tempfile"
version = "3.23.0"

View File

@@ -28,6 +28,7 @@ tauri-plugin-store = "2"
tauri-plugin-window-state = "2"
tauri-plugin-clipboard-manager = "2"
tauri-plugin-http = "2"
tauri-plugin-notification = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

View File

@@ -8,6 +8,10 @@
"opener:default",
"core:window:allow-start-dragging",
"core:webview:allow-set-webview-zoom",
"core:window:allow-is-focused",
"core:window:allow-show",
"core:window:allow-unminimize",
"core:window:allow-set-focus",
"shell:default",
"updater:default",
"dialog:default",
@@ -15,6 +19,7 @@
"store:default",
"window-state:default",
"os:default",
"notification:default",
{
"identifier": "http:default",
"allow": [{ "url": "http://*" }, { "url": "https://*" }, { "url": "http://*:*/*" }]

View File

@@ -198,6 +198,7 @@ pub fn run() {
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_notification::init())
.plugin(PinchZoomDisablePlugin)
.invoke_handler(tauri::generate_handler![
kill_sidecar,

View File

@@ -20,7 +20,7 @@
"plugins": {
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEYwMDM5Nzg5OUMzOUExMDQKUldRRW9UbWNpWmNEOENYT01CV0lhOXR1UFhpaXJsK1Z3aU9lZnNtNzE0TDROWVMwVW9XQnFOelkK",
"endpoints": ["https://github.com/sst/opencode/releases/latest/download/latest.json"]
"endpoints": ["https://github.com/anomalyco/opencode/releases/latest/download/latest.json"]
}
}
}

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