Compare commits

...

626 Commits

Author SHA1 Message Date
opencode
4e7a60dac6 sync release versions for v1.14.51 2026-05-15 00:39:54 +00:00
opencode-agent[bot]
e62ebd8fec chore: generate 2026-05-15 00:02:04 +00:00
Kit Langton
195f592640 refactor(server): simplify listener lifecycle (#27413) 2026-05-15 00:00:52 +00:00
opencode-agent[bot]
78769010a1 chore: generate 2026-05-14 23:46:57 +00:00
Kit Langton
4e143e3a3e test(lib): promote pollWithTimeout/awaitWithTimeout helpers (#27626) 2026-05-14 23:45:32 +00:00
opencode-agent[bot]
dab567aa2d chore: generate 2026-05-14 23:34:42 +00:00
Kit Langton
9d35b04e13 test(acp): replace fixed sleeps with pollUntil in event-subscription (#27624) 2026-05-14 23:33:06 +00:00
Kit Langton
273ab56949 test(bus): fix flaky subscriber races with readiness latch (#27625) 2026-05-14 19:32:25 -04:00
Kit Langton
302ba0ca0b test(session): de-flake shell-cancel tests by waiting for busy state (#27622) 2026-05-14 19:24:09 -04:00
Shoubhit Dash
d35e09f1fc test(workspace): use runtime flags in workspace tests (#27612) 2026-05-15 04:19:39 +05:30
Shoubhit Dash
fc34c74567 refactor(flags): move channel db flag to runtime flags (#27615) 2026-05-15 04:09:10 +05:30
Shoubhit Dash
cb4f5cdea9 refactor(flags): move auto share to runtime flags (#27611) 2026-05-15 03:58:26 +05:30
nv-kasikritc
d34a0194ec feat(provider): add NVIDIA endpoints origin header (#27394) 2026-05-14 17:21:58 -05:00
Shoubhit Dash
43310f4d8c refactor(flags): move embedded web ui flag to runtime flags (#27613) 2026-05-15 03:51:29 +05:30
Shoubhit Dash
e22cfa435a refactor(lsp): move ty flag to runtime flags (#27610) 2026-05-15 03:40:30 +05:30
opencode-agent[bot]
93b1ccc029 chore: generate 2026-05-14 22:00:48 +00:00
Shoubhit Dash
faca2b90c1 refactor(flags): migrate icon discovery runtime flag (#27609) 2026-05-15 03:24:14 +05:30
Shoubhit Dash
76ff18afde refactor(format): move oxfmt flag to runtime flags (#27608) 2026-05-15 03:03:37 +05:30
opencode-agent[bot]
9914c9af17 chore: generate 2026-05-14 21:32:52 +00:00
Shoubhit Dash
f202226bbc refactor(flags): move bash timeout to runtime flags (#27607) 2026-05-15 02:49:14 +05:30
Shoubhit Dash
34198f422c refactor(provider): use runtime flag for experimental models (#27606) 2026-05-15 02:48:01 +05:30
Shoubhit Dash
cccdeef294 refactor(flags): migrate claude code skills flag to RuntimeFlags (#27605) 2026-05-15 02:47:26 +05:30
Musa
83c145f889 fix(plugin): scope digitalocean oauth to genai (#27599) 2026-05-14 15:20:34 -05:00
Kit Langton
d353a6bc24 fix(worktree): accept missing create payload (#27582) 2026-05-14 14:25:22 -04:00
bo-tato
d25cc42d21 docs(app): stale reference to removed multi-edit tool (#27579) 2026-05-14 13:10:01 -05:00
opencode-agent[bot]
6039b894c5 chore: generate 2026-05-14 18:05:32 +00:00
Kit Langton
b4fc5ef071 refactor(http-recorder): tighten cassette safety, fix WS leaks + docs (#26730) 2026-05-14 18:03:22 +00:00
opencode-agent[bot]
f6c8e35383 chore: generate 2026-05-14 17:58:35 +00:00
Kit Langton
94564f3588 fix(session): prevent double auto-compaction from filterCompacted reorder (#27545) 2026-05-14 13:56:12 -04:00
Kit Langton
855bda8384 test(question): wait on question events (#27124) 2026-05-14 13:23:47 -04:00
opencode-agent[bot]
756488d534 chore: generate 2026-05-14 16:42:18 +00:00
Shoubhit Dash
22de34c4de feat: add experimental background subagents (#27084) 2026-05-14 22:10:15 +05:30
Adam
bdb0c16a93 chore: update web stats 2026-05-14 11:26:59 -05:00
Sameer Kankute
7f7eb2e7f8 fix(provider): remove LiteLLM workarounds ported upstream, requires LiteLLM v1.85.0-rc.2+ (#26819)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-14 11:26:07 -05:00
opencode-agent[bot]
e15fd0bb93 chore: generate 2026-05-14 13:33:44 +00:00
opencode-agent[bot]
8f90697df8 chore: generate 2026-05-14 13:32:19 +00:00
opencode-agent[bot]
17af25d1c1 chore: generate 2026-05-14 13:30:59 +00:00
Kit Langton
3c81326a5e docs(effect): refresh TODO with shipped P0 and RF work (#27536) 2026-05-14 09:29:32 -04:00
opencode-agent[bot]
9f8d8f5b0e chore: generate 2026-05-14 12:40:10 +00:00
OpeOginni
337993d53e feat(desktop): add mcp client registration status and authentication handling (#27525) 2026-05-14 20:38:52 +08:00
Shoubhit Dash
e26abd8da9 fix(tool): close shell truncation stream (#27517) 2026-05-14 16:34:42 +05:30
Shoubhit Dash
8c1ce0b80c refactor(flags): simplify tui plugin runtime flags (#27506) 2026-05-14 15:56:02 +05:30
Brendan Allan
f8c3f560d4 fix(desktop): await execFilePromise and read stdout properly (#27499) 2026-05-14 17:52:23 +08:00
Shoubhit Dash
7e43d3e3f5 refactor(lsp): type initialize errors (#27494) 2026-05-14 15:20:02 +05:30
opencode-agent[bot]
52db7a76e2 chore: update nix node_modules hashes 2026-05-14 09:28:30 +00:00
Shoubhit Dash
be6e7b309e refactor(provider): type init errors (#27484) 2026-05-14 14:48:58 +05:30
Simon Klee
0af242974c deps: Upgrade OpenTUI to 0.2.10 (#27491) 2026-05-14 09:14:03 +00:00
Shoubhit Dash
27ac53aaac fix(server): stop exposing named defects (#27471) 2026-05-14 12:51:05 +05:30
Shoubhit Dash
78015571bf refactor(server): centralize session busy mapping (#27473) 2026-05-14 12:50:36 +05:30
Aiden Cline
e76cf967e6 fix(session): finalize interrupted assistant messages (#27254) 2026-05-14 01:19:11 -05:00
opencode-agent[bot]
c2723b5ea0 chore: generate 2026-05-14 04:25:32 +00:00
Frederik
9675579796 fix: bug encountered when using azure gpt-5.5 w/ completions api (#26222) 2026-05-13 23:24:16 -05:00
opencode-agent[bot]
4d8368970a chore: update nix node_modules hashes 2026-05-14 04:20:42 +00:00
Nikhil Patel
2a7af6acd8 fix(tui): preserve text selection on question prompt options (#24988)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-05-13 23:17:59 -05:00
opencode-agent[bot]
bfd707abc9 chore: generate 2026-05-14 04:07:30 +00:00
Aiden Cline
981e00971a fix: image resizer wasm loading, reenable image resizing (#26805) 2026-05-13 23:06:07 -05:00
Aiden Cline
c50d2b3656 Refactor event HTTP API route modules (#27441) 2026-05-13 22:41:17 -05:00
opencode
ddad0988e7 sync release versions for v1.14.50 2026-05-14 03:03:23 +00:00
Kit Langton
cda8cc7285 test(httpapi): simplify event stream regression coverage (#27427) 2026-05-14 02:18:40 +00:00
Kit Langton
b928a1fff9 fix(httpapi): preserve event stream context (#27425)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: James Long <longster@gmail.com>
2026-05-13 22:02:30 -04:00
Kit Langton
04286d0415 docs(effect): plan Instance deletion path (#27424) 2026-05-13 21:58:54 -04:00
opencode-agent[bot]
33bb33ba90 chore: generate 2026-05-14 01:55:00 +00:00
Sebastian
b0ade40265 flip back to markdown renderable (#27421) 2026-05-14 03:53:48 +02:00
Kit Langton
681594b551 refactor(storage): remove not found wire serializer (#27416) 2026-05-13 21:39:50 -04:00
Kit Langton
edf7649400 fix(session): type busy errors (#27410) 2026-05-14 01:28:04 +00:00
Kit Langton
3fc7486d15 test(session): fix shell-cancel race when trap hasn't installed yet (#27408) 2026-05-14 01:10:40 +00:00
Kit Langton
8e353584c7 test(format): remove formatter check sleeps (#27407) 2026-05-13 21:02:34 -04:00
Sebastian
5c35ea2181 notification docs (#27406) 2026-05-14 03:01:25 +02:00
opencode-agent[bot]
faf8713053 chore: generate 2026-05-14 00:59:33 +00:00
Dax
16c457e712 refactor(core): move models.dev into core (#27347) 2026-05-13 20:58:24 -04:00
Kit Langton
9818c9e8d0 fix(provider): make small model fallback optional (#27405) 2026-05-14 00:44:34 +00:00
Kit Langton
5e41dbbcbf test(effect): use Effect sleep in instance state tests (#27404) 2026-05-13 20:43:46 -04:00
Kit Langton
ba5c8d3822 fix(llm): preserve tool error defects (#27403) 2026-05-13 20:43:32 -04:00
opencode-agent[bot]
10c90eb445 chore: generate 2026-05-14 00:33:32 +00:00
Kit Langton
aa8a41d1b8 effect(patch,tool): migrate patch/index and tool/read to AppFileSystem (#27155) 2026-05-13 20:32:19 -04:00
Kit Langton
3f33be1928 effect(server): typed errors in session/sync handlers, fix concurrency (#27146) 2026-05-13 20:31:27 -04:00
Kit Langton
42e6b7d541 effect(core): track stderr truncation; polish AppProcess callers (#27353) 2026-05-13 20:31:03 -04:00
Kit Langton
ccb207f946 effect(util): migrate filesystem callers to AppFileSystem.Service (#27152) 2026-05-13 20:25:37 -04:00
Kit Langton
de1e0b5d6d test(workspace): effectify sync state cases (#27400) 2026-05-13 19:49:03 -04:00
Aiden Cline
df3895d74f cleanup: make smallOptions rely on variants (#27390) 2026-05-13 18:30:36 -05:00
Kit Langton
55e0af1405 fix(provider): type model not found errors (#27334) 2026-05-13 22:23:09 +00:00
Kit Langton
5182a3698d test(workspace): use Effect for local session warp cases (#27393) 2026-05-13 22:22:51 +00:00
opencode
73e1de4513 sync release versions for v1.14.49 2026-05-13 22:18:40 +00:00
Frank
44b432c3fd sync 2026-05-13 18:05:06 -04:00
Kit Langton
0d8c9f3437 docs: add LayerMap example (#27388) 2026-05-13 17:46:47 -04:00
opencode-agent[bot]
0d074492df chore: update nix node_modules hashes 2026-05-13 21:20:10 +00:00
Sebastian
3b7a5e783d fix keymap fallback priority and TUI config diagnostics (#27384) 2026-05-13 23:00:48 +02:00
Frank
c197fd92b7 sync 2026-05-13 15:58:34 -04:00
Shoubhit Dash
9ee1f6ceba fix(server): map busy sessions in http handlers (#27375) 2026-05-14 01:02:07 +05:30
Shoubhit Dash
20cec91550 fix(provider): restore model suggestions (#27372) 2026-05-14 00:38:38 +05:30
Aiden Cline
22a5e6cc50 fix(run): restore non-interactive exit behavior (#27371) 2026-05-13 18:45:34 +00:00
Shoubhit Dash
52f9bcbb82 refactor(flags): route installation client through runtime flags (#27369) 2026-05-14 00:10:31 +05:30
Shoubhit Dash
a4ebb07c25 refactor(flags): route llm client through runtime flags (#27368) 2026-05-14 00:09:53 +05:30
opencode-agent[bot]
7cc968b05d chore: generate 2026-05-13 16:47:21 +00:00
Frank
fa077b92b1 zen: update sticky session logic 2026-05-13 12:45:11 -04:00
Kit Langton
8ad3a4b217 test(util): migrate log cleanup test to Effect (#27357) 2026-05-13 16:43:23 +00:00
Kit Langton
533495ae20 test(mcp): migrate OAuth auto-connect tests (#27356) 2026-05-13 16:38:37 +00:00
Kit Langton
f0635e365f test(session): use Effect polling in processor tests (#27354) 2026-05-13 16:33:19 +00:00
Kit Langton
25de3e407b test(acp): use shared instance fixture for event tests (#27351) 2026-05-13 12:30:13 -04:00
Frank
655b25bccf sync 2026-05-13 12:05:30 -04:00
Kit Langton
e5d13d9519 effect(git): migrate to AppProcess.run (#27190) 2026-05-13 12:04:51 -04:00
Kit Langton
5cdbb7505e effect(installation): migrate to AppProcess.run (#27188) 2026-05-13 11:54:29 -04:00
Kit Langton
e5319846ad test(server): migrate pty websocket input test (#27348) 2026-05-13 15:43:09 +00:00
Kit Langton
832aa94977 effect(worktree): migrate to AppProcess.run (#27186) 2026-05-13 11:39:35 -04:00
Kit Langton
6c7f35b49e effect(format): migrate to AppProcess (#27185) 2026-05-13 15:26:42 +00:00
Kit Langton
6e25720307 test(tool): use Effect sleep in edit concurrency test (#27349) 2026-05-13 15:18:35 +00:00
Kit Langton
ca723f1cbc effect(core): add stdin option to AppProcess.run; migrate snapshot+clipboard (#27224) 2026-05-13 11:10:23 -04:00
Kit Langton
650f67a05a chore: delete unused util/lock module (#27223) 2026-05-13 15:08:35 +00:00
opencode-agent[bot]
50dccac915 chore: generate 2026-05-13 15:06:14 +00:00
Kit Langton
02b8b0ff93 test: migrate file watcher test to Effect (#27346) 2026-05-13 15:04:30 +00:00
Kit Langton
76c91c6e33 test: migrate mcp oauth browser tests (#27345) 2026-05-13 15:04:02 +00:00
opencode-agent[bot]
ca17ca85cd chore: update nix node_modules hashes 2026-05-13 15:02:16 +00:00
opencode-agent[bot]
e7aed64949 chore: generate 2026-05-13 14:59:13 +00:00
Dax Raad
d43124abe0 ignore: notes 2026-05-13 10:57:22 -04:00
Kit Langton
7404664827 refactor: migrate installation tests to testEffect (#27342) 2026-05-13 14:56:51 +00:00
Kit Langton
0b112e5bcf test: migrate permission task config tests (#27343) 2026-05-13 14:56:26 +00:00
Shoubhit Dash
268d758130 refactor(flags): route control-plane workspaces through runtime flags (#27337) 2026-05-13 20:22:30 +05:30
opencode-agent[bot]
72acdf0505 chore: generate 2026-05-13 14:50:34 +00:00
Shoubhit Dash
e28ef7b57c refactor(flags): route sync workspaces through runtime flags (#27336) 2026-05-13 20:18:06 +05:30
opencode-agent[bot]
5b5376a3fa chore: generate 2026-05-13 14:47:40 +00:00
Kit Langton
766318a4cf effect(snapshot): migrate to AppProcess.run (#27189) 2026-05-13 10:46:14 -04:00
Kit Langton
8d5aa584b4 test(workspace): effectify sync start coverage (#27338) 2026-05-13 14:45:43 +00:00
Shoubhit Dash
eed0eddc63 refactor(flags): route session workspaces through runtime flags (#27335) 2026-05-13 20:14:40 +05:30
Dax
8345152319 core: expose v2 model listing API (#25821) 2026-05-13 14:43:08 +00:00
Kit Langton
bebe5442a5 chore: delete unused util/color module (#27331) 2026-05-13 14:02:17 +00:00
Kit Langton
bc25342f34 chore: delete util/scrap module (#27330) 2026-05-13 14:01:19 +00:00
Kit Langton
762020f63a chore: delete unused util/network module (#27329) 2026-05-13 13:56:15 +00:00
Shoubhit Dash
f13fc5a8a8 refactor(flags): route event system through runtime flags (#27323) 2026-05-13 17:44:09 +05:30
Shoubhit Dash
098bdd8ae2 refactor(flags): route plan mode through runtime flags (#27320) 2026-05-13 17:17:04 +05:30
Shoubhit Dash
4d205027ca refactor(flags): route scout through runtime flags (#27318) 2026-05-13 17:07:53 +05:30
opencode-agent[bot]
5975547c84 chore: generate 2026-05-13 11:19:45 +00:00
Shoubhit Dash
5b2b300602 fix(session): tighten http error contracts (#27308) 2026-05-13 16:48:18 +05:30
opencode-agent[bot]
d488e3fd2a chore: generate 2026-05-13 10:18:40 +00:00
Shoubhit Dash
809af5c590 fix(provider): type auth errors (#27301) 2026-05-13 15:47:13 +05:30
vimtor
733bd3c74e chore: activate low tps alerts 2026-05-13 12:12:03 +02:00
opencode-agent[bot]
374951bf60 chore: generate 2026-05-13 09:39:54 +00:00
Victor Navarro
2e94f505a4 chore: add low tps model alerts (#27055) 2026-05-13 09:38:31 +00:00
opencode-agent[bot]
4498fc983d chore: generate 2026-05-13 09:36:51 +00:00
Shoubhit Dash
2e7cf92c8b fix(worktree): type expected errors (#27296) 2026-05-13 15:05:30 +05:30
Shoubhit Dash
ccf93f3523 fix(session): make message reads effectful (#27291) 2026-05-13 14:40:12 +05:30
Shoubhit Dash
4b041716fc fix(server): remove storage not found defect fallback (#27287) 2026-05-13 14:15:53 +05:30
Shoubhit Dash
b0dc8e4638 fix(session): use typed message reads in tools (#27280) 2026-05-13 14:12:51 +05:30
OpeOginni
596f241db5 fix(app): enhance error handling by unwrapping SDK-wrapped errors in formatServerError (#27061) 2026-05-13 13:53:10 +05:30
Luke Parker
3a810fcb9a perf(ui): render icons through an svg sprite (#26950) 2026-05-13 17:57:13 +10:00
opencode-agent[bot]
e5af7ab99a chore: generate 2026-05-13 07:53:28 +00:00
Shoubhit Dash
f01c6b3e37 fix(session): type message list not found errors (#27275) 2026-05-13 13:22:12 +05:30
Shoubhit Dash
fed043a1ad fix(session): add typed message lookup wrappers (#27269) 2026-05-13 13:04:07 +05:30
Shoubhit Dash
e9a29e4908 fix(storage): type not found errors (#27265) 2026-05-13 12:25:04 +05:30
Brendan Allan
d93a06431e refactor(app): clarify session_working logic in child-store (#27267) 2026-05-13 14:36:00 +08:00
opencode-agent[bot]
9fe912439b chore: generate 2026-05-13 05:41:29 +00:00
Shoubhit Dash
dd46fddf22 test(cli): cover config json diagnostics (#27257) 2026-05-13 05:40:12 +00:00
opencode-agent[bot]
d3d7b44e73 chore: generate 2026-05-13 05:32:32 +00:00
Shoubhit Dash
367665dba2 fix(cli): render tagged config errors (#27256) 2026-05-13 11:01:18 +05:30
Brendan Allan
4aaece29d9 feat(desktop): reintroduce AppStream MetaInfo for Linux desktop builds (#27253) 2026-05-13 13:14:39 +08:00
opencode-agent[bot]
bed88ce50e chore: generate 2026-05-13 04:56:52 +00:00
Aiden Cline
28204720dc temporarily revert: preserve permission ordering by accepting a layered array (#27258) 2026-05-12 23:55:45 -05:00
Brendan Allan
10b99b2f5a build(ci): use native arm64 runner for desktop linux arm64 builds (#27250) 2026-05-13 12:52:18 +08:00
Shoubhit Dash
90c13d93f3 fix(server): hide unknown 500 details (#27251) 2026-05-13 10:15:28 +05:30
Aiden Cline
67e6408cef fix: disable image module for now (#27248) 2026-05-12 23:21:31 -05:00
Brendan Allan
3be65dff46 fix: add optional chaining to session_status access (#27247) 2026-05-13 12:09:36 +08:00
opencode-agent[bot]
c5961205ef chore: update nix node_modules hashes 2026-05-13 03:44:43 +00:00
Brendan Allan
ad6a8a1850 fix: use htmlrewriter2 instead of HTMLRewriter for node compat (#26309) 2026-05-13 03:28:08 +00:00
Kit Langton
46daede10c test(pty): migrate output isolation to Effect runner (#27235) 2026-05-13 03:25:52 +00:00
Kit Langton
8249baeb4e test(pty): migrate shell tests to Effect runner (#27238) 2026-05-13 03:20:20 +00:00
Frank
77e51b0a32 zen: stat worker 2026-05-12 23:19:25 -04:00
opencode-agent[bot]
784796e27a chore: generate 2026-05-13 03:14:19 +00:00
Kit Langton
913ea36ae6 test(server): scope MCP HttpApi handler (#27226) 2026-05-13 03:13:05 +00:00
Kit Langton
68c4951318 test(mcp): migrate lifecycle tests to Effect runner (#27205) 2026-05-13 03:05:30 +00:00
Kit Langton
6d3b2fe08b test(server): stabilize SDK project skill prompt test (#27239) 2026-05-13 03:01:29 +00:00
Kit Langton
d88cef6ada test(mcp): migrate headers tests to Effect runner (#27237) 2026-05-13 02:56:29 +00:00
Kit Langton
8370d0cef4 test(server): migrate httpapi ui tests to effect runner (#27236) 2026-05-13 02:55:36 +00:00
Kit Langton
0b67e1a046 test(server): migrate session messages to Effect runner (#27234) 2026-05-13 02:55:33 +00:00
Kit Langton
485ecbde18 test(server): migrate global session list to effect runner (#27233) 2026-05-13 02:52:59 +00:00
Kit Langton
03cf833236 test(provider): migrate DigitalOcean provider test to Effect runner (#27232) 2026-05-13 02:51:11 +00:00
Kit Langton
e3684f36f9 chore: delete unused util/abort module + orphaned leak test (#27230) 2026-05-13 02:46:41 +00:00
Kit Langton
13fbc9acfc docs(effect): add cleanup roadmap (#27228) 2026-05-13 02:45:26 +00:00
Kit Langton
588b5240d0 test(server): migrate worktree endpoint repro to effect runner (#27220) 2026-05-13 02:43:41 +00:00
qunqin
80543fb5dc fix(desktop): resolve login shell when loading env (#26449)
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
2026-05-13 10:40:36 +08:00
Kit Langton
ff16eb8dea test(project): use Deferred for dispose handoff (#27225) 2026-05-13 02:32:50 +00:00
Kit Langton
91a9514ed0 test(server): use AppFileSystem in provider tests (#27227) 2026-05-13 02:32:30 +00:00
Brendan Allan
2f4dce789f app: use session_working helper to simplify loading states (#27212) 2026-05-13 10:27:05 +08:00
opencode-agent[bot]
b43147440c chore: generate 2026-05-13 02:16:48 +00:00
Kit Langton
c96a77c60b test(pty): migrate session tests to Effect runner (#27222) 2026-05-13 02:15:38 +00:00
opencode-agent[bot]
6cd2a743b5 chore: generate 2026-05-13 02:10:48 +00:00
Luke Parker
0f8517208b perf(app): unmount closed review panel (#27221) 2026-05-13 12:09:34 +10:00
Kit Langton
0879f5e22d test(server): migrate project init git test to Effect runner (#27218) 2026-05-13 02:08:55 +00:00
opencode-agent[bot]
16333b5222 chore: generate 2026-05-13 02:08:16 +00:00
Kit Langton
e16f4b6b99 test(permission): migrate next tests to effect runner (#27217) 2026-05-13 02:06:58 +00:00
Kit Langton
6fbb08b724 test(project): migrate instance tests to effect runner (#27215) 2026-05-13 02:06:16 +00:00
Kit Langton
a26a2a95bd test(server): migrate provider httpapi test to effect runner (#27216) 2026-05-13 02:05:52 +00:00
Kit Langton
7f9268f147 test(server): migrate global bus helper to Effect (#27214) 2026-05-13 02:04:46 +00:00
Kit Langton
5c7af682b9 test(server): migrate MCP HTTP API test to Effect runner (#27213) 2026-05-13 02:03:52 +00:00
Kit Langton
911b2ac088 test(session): migrate prompt tests to effect runner (#27209) 2026-05-13 01:51:35 +00:00
Kit Langton
da689d77c4 effect: move tool flags into RuntimeFlags (#27198) 2026-05-12 21:45:53 -04:00
opencode-agent[bot]
c9df833d2c chore: generate 2026-05-13 01:45:20 +00:00
Aiden Cline
c2b1ebd9dc feat: update pricing schema for models to ensure more accurate cost tracking (#27184) 2026-05-12 20:44:06 -05:00
Kit Langton
d1356f509e test(server): migrate HTTP API SDK test to Effect runner (#27208) 2026-05-13 01:43:35 +00:00
opencode-agent[bot]
69feb224ba chore: update nix node_modules hashes 2026-05-13 01:43:08 +00:00
Kit Langton
7d9ac713a9 test(config): migrate TUI config tests to Effect runner (#27206) 2026-05-13 01:42:33 +00:00
Kit Langton
4d9c675b00 test(server): migrate httpapi session test to effect runner (#27207) 2026-05-13 01:40:25 +00:00
Kit Langton
604de70689 test(server): migrate experimental HttpApi test to Effect (#27204) 2026-05-13 01:40:07 +00:00
opencode-agent[bot]
673226d214 chore: generate 2026-05-13 01:39:46 +00:00
Kit Langton
2447f42294 test(server): migrate session select to effect runner (#27203) 2026-05-13 01:38:38 +00:00
Frank
005e64e7d8 zen: stat worker 2026-05-12 21:37:41 -04:00
Kit Langton
8310e7df70 test(server): migrate missing patch diff test (#27202) 2026-05-13 01:37:34 +00:00
Kit Langton
c4e676b8a0 fix(task): preserve subagent self permissions (#27201) 2026-05-13 01:36:02 +00:00
opencode-agent[bot]
c2c40b5b61 chore: generate 2026-05-13 01:28:35 +00:00
Kit Langton
456495289e fix(httpapi): drop redundant InstanceRef/WorkspaceRef in session prompt handler (#27196) 2026-05-13 01:27:08 +00:00
Sebastian
d6367853ae Add TUI notifications and attention sounds (disabled by default) (#26980) 2026-05-13 03:26:25 +02:00
opencode-agent[bot]
adccab5970 chore: generate 2026-05-13 01:26:12 +00:00
Kit Langton
b6d3fa09bc effect(core): add AppProcess service (Phase 1) (#27178) 2026-05-13 01:24:53 +00:00
Kit Langton
31e2d72bf7 test(worktree): scope created worktree cleanup (#27191) 2026-05-12 21:09:58 -04:00
Kit Langton
b7c6fa611f effect: add RuntimeFlags service (#27181) 2026-05-12 20:59:02 -04:00
opencode-agent[bot]
d0b8ff0f22 chore: update nix node_modules hashes 2026-05-13 00:57:32 +00:00
Kit Langton
59b976b954 Remove TUI logo sound effects (#27183) 2026-05-12 20:37:16 -04:00
Luke Parker
d1640c075b fix(app): use session status for busy state (#27166) 2026-05-13 10:25:28 +10:00
Kit Langton
17d4c366e6 test(worktree): dispose created instances before removal (#27182) 2026-05-12 20:23:20 -04:00
Kit Langton
d0844c600b test(worktree): use timeoutOrElse for ready wait (#27180) 2026-05-12 20:14:05 -04:00
Dax Raad
1d243ce25a harden cache usage 2026-05-12 20:06:46 -04:00
Kit Langton
d8c8322ddf test: migrate worktree tests to effect runner (#27177) 2026-05-12 19:55:07 -04:00
Kit Langton
1b6599f411 test(plugin): use noop dependency boundaries (#27148) 2026-05-12 19:53:46 -04:00
opencode-agent[bot]
937a3c10b3 chore: generate 2026-05-12 23:49:28 +00:00
Kit Langton
72ce24c200 test: migrate effect cmd ALS test (#27173) 2026-05-12 23:48:17 +00:00
Kit Langton
3301fad8cd test: migrate app runtime logger effect tests (#27176) 2026-05-12 23:47:49 +00:00
Kit Langton
9c54255aeb fix: migrate sync http api test to effect runner (#27175) 2026-05-12 23:47:14 +00:00
Kit Langton
b0674b473f test: migrate session action route to effect runner (#27174) 2026-05-12 23:46:51 +00:00
Kit Langton
ded4da735b test: migrate webfetch tool tests to effect runner (#27171) 2026-05-12 23:46:23 +00:00
Kit Langton
2c334d9242 Migrate schema error body tests to Effect runner (#27172) 2026-05-12 23:46:19 +00:00
Kit Langton
81dd46abec test: migrate websearch tests to effect runner (#27170) 2026-05-12 23:45:01 +00:00
opencode-agent[bot]
baef5cd43b chore: generate 2026-05-12 22:11:13 +00:00
Andrew Suffield
65368f609d fix: preserve permission ordering by accepting a layered array (#23214)
Co-authored-by: Andrew Suffield <asuffield@cloudflare.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-05-12 17:09:06 -05:00
opencode-agent[bot]
2cb697b720 chore: generate 2026-05-12 21:24:39 +00:00
Aiden Cline
cb511f78ff fix(plugin): preserve tool attachments (#27157) 2026-05-12 16:23:15 -05:00
Musa
159964b172 feat(plugin): add DigitalOcean OAuth + Inference Routers (#26095) 2026-05-12 16:22:40 -05:00
Kit Langton
d9f9f1553b test: use Effect file services in migrated tests (#27154) 2026-05-12 20:48:07 +00:00
Kit Langton
0fb55b4f1a test(project): migrate global project tests to Effect runner (#27142) 2026-05-12 20:45:39 +00:00
Kit Langton
3c34f6704b test: migrate auth override plugin test (#27140) 2026-05-12 20:41:34 +00:00
Kit Langton
4cf088ae84 test: migrate instance bootstrap to Effect runner (#27144) 2026-05-12 20:34:12 +00:00
Aiden Cline
ca28dd02ec fix(compaction): restore tail turns after summarization (#27145) 2026-05-12 15:33:16 -05:00
Kit Langton
2017dc165c test: migrate negative tokens regression to Effect runner (#27141) 2026-05-12 20:32:23 +00:00
Kit Langton
dc9d6a08cb test: migrate agent color config tests (#27139) 2026-05-12 20:31:38 +00:00
Kit Langton
af4ab017cb test(session): migrate structured output integration test (#27143) 2026-05-12 20:30:35 +00:00
Kit Langton
dd14413a64 Preserve native LLM tool context (#27116) 2026-05-12 16:16:58 -04:00
Frank
b9e7cbf13c sync 2026-05-12 16:06:19 -04:00
Kit Langton
3f74abc6cd test: simplify Effect migration follow-ups (#27136) 2026-05-12 20:00:54 +00:00
Kit Langton
e0d0fe1ff7 test(bus): migrate integration tests to Effect runner (#27132) 2026-05-12 19:58:54 +00:00
opencode-agent[bot]
f7dbb4dac4 chore: generate 2026-05-12 19:48:35 +00:00
Kit Langton
c5849e56cc test(project): migrate project tests to Effect runner (#27134) 2026-05-12 19:47:17 +00:00
opencode-agent[bot]
e46ab34d27 chore: generate 2026-05-12 19:44:26 +00:00
Kit Langton
1d4613006a test(project): migrate instance tests to Effect runner (#27130) 2026-05-12 19:41:46 +00:00
Kit Langton
71040c54aa test(plugin): migrate loader shared tests to Effect runner (#27129) 2026-05-12 19:41:44 +00:00
Kit Langton
fec78154b5 test(bus): migrate bus tests to Effect runner (#27131) 2026-05-12 19:41:24 +00:00
Kit Langton
3e2ec192cf test(question): remove WithInstance bridge (#27128) 2026-05-12 19:40:01 +00:00
Kit Langton
ec960da42a test(skill): migrate discovery tests to Effect runner (#27127) 2026-05-12 19:39:03 +00:00
Shoubhit Dash
45de4975de refactor(core): resolve default agent info (#27125) 2026-05-13 01:08:30 +05:30
Kit Langton
e540daabc4 test(agent): migrate plan bypass tests to Effect runner (#27119) 2026-05-12 14:56:01 -04:00
opencode-agent[bot]
f3c91c5f96 chore: generate 2026-05-12 18:52:25 +00:00
Kit Langton
549b146ea6 Stabilize session event tests (#27117) 2026-05-12 18:51:18 +00:00
Kit Langton
3c7569d852 test(tool): migrate external directory tests to Effect runner (#27122) 2026-05-12 18:50:56 +00:00
Kit Langton
8f1ded9e08 test(file): migrate ripgrep tests to Effect runner (#27120) 2026-05-12 18:50:26 +00:00
Kit Langton
b668af29dd test(git): migrate git tests to Effect runner (#27121) 2026-05-12 18:50:07 +00:00
Kit Langton
ec30ff9120 test(agent): migrate agent tests to Effect runner (#27118) 2026-05-12 18:49:30 +00:00
opencode-agent[bot]
a3714d4399 chore: update nix node_modules hashes 2026-05-12 18:27:42 +00:00
Kit Langton
822eec0d62 Fix runner cancel completion (#27115) 2026-05-12 14:22:56 -04:00
Kit Langton
3974520742 Migrate UI cancel error to tagged error (#27112) 2026-05-12 18:09:00 +00:00
Kit Langton
fda37b3609 Remove Zod from app global SDK (#27111) 2026-05-12 14:02:12 -04:00
Kit Langton
6b950b666a Remove Zod from core dependencies (#27107) 2026-05-12 13:51:08 -04:00
Kit Langton
bc4fdb8370 Remove unused app ID schema (#27105) 2026-05-12 13:09:23 -04:00
Kit Langton
2b9af91568 Remove Zod from core log (#27102) 2026-05-12 13:08:57 -04:00
Kit Langton
53a3f95088 Make core fn Zod import type-only (#27103) 2026-05-12 12:58:50 -04:00
Dax Raad
5a4596c879 core: Wait 3 days before installing new package versions to reduce supply chain risk 2026-05-12 12:50:32 -04:00
opencode-agent[bot]
0ce614a280 chore: generate 2026-05-12 16:46:35 +00:00
Kit Langton
e8125e9b42 test(server): migrate session list tests to Effect runner (#27101) 2026-05-12 16:45:05 +00:00
Kit Langton
a7b5041674 test(file): migrate fsmonitor tests to Effect runner (#27099) 2026-05-12 16:44:04 +00:00
Kit Langton
a16789dfdd test(tool): migrate apply patch tests to Effect runner (#27100) 2026-05-12 16:43:33 +00:00
Kit Langton
8115004c73 test(file): migrate path traversal tests to Effect runner (#27098) 2026-05-12 16:42:49 +00:00
Kit Langton
ec4fdaf8e9 test(tool): migrate tool define tests to Effect runner (#27097) 2026-05-12 16:42:19 +00:00
Shoubhit Dash
3dc2c1d81c fix(session): preserve usage update timestamps (#27094) 2026-05-12 22:10:28 +05:30
Kit Langton
d658e1e350 Remove local MCP Zod schema (#27095) 2026-05-12 12:38:27 -04:00
opencode-agent[bot]
30e3fa1de9 chore: generate 2026-05-12 16:33:06 +00:00
Aiden Cline
23f8b3eb3e fix: annotate Effect log metadata (#27093) 2026-05-12 11:31:18 -05:00
Kit Langton
c7d8b0d565 Delete named schema error wrapper (#27066) 2026-05-12 12:04:28 -04:00
Kit Langton
257fcafc83 test(tool): migrate edit concurrency test (#26983) 2026-05-12 11:52:31 -04:00
Kit Langton
04aafe2bfc test(provider): migrate more config-backed cases (#27067) 2026-05-12 14:19:49 +00:00
opencode-agent[bot]
0fd0facc44 chore: generate 2026-05-12 13:31:03 +00:00
Kit Langton
0de3b67cc0 test(tool): migrate shell tests to Effect runner (#26968) 2026-05-12 13:28:46 +00:00
Kit Langton
28f38fc871 Remove Zod from named errors (#26982) 2026-05-12 09:20:15 -04:00
Shoubhit Dash
8feb4a31c7 feat(core): add background job service (#27033) 2026-05-12 15:22:38 +05:30
Simon Klee
8f05bbfaa6 prompt: fix cursor math for wide characters (#27017)
String.length counts code points, not display columns, so CJK
characters and emoji that occupy two terminal cells caused
misaligned cursors, broken mention triggers, and incorrect
history restoration offsets.

Use Bun.stringWidth for now, we need an alternative for this.

Fix #26716
Close #26922
2026-05-12 11:45:28 +02:00
Brendan Allan
d276d96cdf fix(app): remember selected model variant when switching sessions/projects (#27029) 2026-05-12 17:44:50 +08:00
Luke Parker
caf1151cb5 refactor(app): centralize sync query options (#25941)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
2026-05-12 16:40:21 +08:00
Brendan Allan
ff38bbeeeb refactor(desktop): remove configureEnv callback from spawnLocalServer (#27022) 2026-05-12 16:39:56 +08:00
Shoubhit Dash
2481dde36d chore: remove codesearch tool (#27019) 2026-05-12 13:44:02 +05:30
Matt H
61174b7878 fix(tui): make websearch provider label reactive (#26943) 2026-05-12 08:01:16 +00:00
opencode-agent[bot]
907281a80a chore: generate 2026-05-12 06:44:09 +00:00
Brendan Allan
3992e2a76b feat(app): add ctrl/cmd+number keybinds to switch projects (#26280) 2026-05-12 14:43:07 +08:00
opencode-agent[bot]
ea6eabe1d9 chore: generate 2026-05-12 05:20:22 +00:00
Dax
36d40fee4d Track session usage totals (#26644) 2026-05-12 01:18:57 -04:00
James Long
e36bc20f84 fix(tui): fix flicker by avoiding redundant workspace session sync (#26997) 2026-05-12 00:30:03 -04:00
Aiden Cline
487575773d feat: create global opencode.jsonc if no configs exist (#26992) 2026-05-11 23:13:22 -05:00
opencode-agent[bot]
5cc84800dc chore: generate 2026-05-12 03:43:40 +00:00
James Long
2b432d9e03 fix(tui): scope events by project (#26936) 2026-05-11 23:42:04 -04:00
opencode-agent[bot]
591eb667d5 chore: generate 2026-05-12 03:26:47 +00:00
Aiden Cline
ddce776225 ignore: add codebase skill to repo (#26990) 2026-05-12 03:25:41 +00:00
Aiden Cline
1a28924ed8 fix: grep external directory permission evaluation (#26958) 2026-05-11 21:47:35 -05:00
Brendan Allan
871374804f fix(app): use keyed Show for project in layout (#26985) 2026-05-12 10:39:36 +08:00
Brendan Allan
78a2639e5e fix(app): open next project when closing current one (#26987) 2026-05-12 10:38:21 +08:00
Kit Langton
cc1835e0db test(provider): migrate config-backed cases to Effect runner (#26969) 2026-05-12 02:23:52 +00:00
Kit Langton
0f5d4ae648 test(project): stabilize VCS branch update test (#26979) 2026-05-11 22:12:07 -04:00
Kit Langton
ce72020750 test(tool): migrate edit tests to Effect runner (#26977) 2026-05-11 21:42:17 -04:00
Kit Langton
c43d606f8e agent: use Effect schema for generated agent object (#26973) 2026-05-11 21:42:04 -04:00
Kit Langton
1007630347 Migrate runtime validators to Effect Schema (#26975) 2026-05-11 21:41:56 -04:00
Kit Langton
9e8274d2da Remove internal Zod schemas (#26974) 2026-05-11 21:40:44 -04:00
Kit Langton
74aa735e6a fix(tui): guard prompt submit against concurrent invocation (#26972) 2026-05-12 01:35:28 +00:00
Kit Langton
8030a6c187 Emit LLM stream lifecycle events (#26971) 2026-05-11 21:31:48 -04:00
Kit Langton
e5aa5161f2 Remove effect-zod bridge (#26956) 2026-05-11 21:14:55 -04:00
Kit Langton
abb1ee6278 docs(test): add Effect migration orchestration notes (#26963) 2026-05-11 20:59:51 -04:00
Kit Langton
c4003579bb test(project): migrate VCS tests to Effect runner (#26965) 2026-05-11 20:59:34 -04:00
Kit Langton
0d9c534184 test(snapshot): migrate snapshot tests to Effect runner (#26964) 2026-05-11 20:59:31 -04:00
Aiden Cline
5773d43cbf ci: GitHub Actions dependencies (#26962) 2026-05-11 19:50:35 -05:00
opencode-agent[bot]
e0e9414cbd chore: generate 2026-05-12 00:41:30 +00:00
Kit Langton
44edb639c2 test(session): migrate message pagination to Effect runner (#26957) 2026-05-12 00:40:23 +00:00
Kit Langton
fbd52ca2f4 test(file): migrate file tests to Effect runner (#26959) 2026-05-12 00:39:31 +00:00
opencode-agent[bot]
8015ff7ca5 chore: generate 2026-05-12 00:33:34 +00:00
Kit Langton
ec9584177f docs(test): plan Effect test migration (#26954) 2026-05-11 20:32:27 -04:00
Kit Langton
061efc6cf2 Fix run JSON output draining (#26955) 2026-05-11 20:30:23 -04:00
Brendan Allan
fe374aea46 feat(app): persist todo dock collapsed state (#26953) 2026-05-11 23:59:38 +00:00
Kit Langton
46edc98f10 Validate TUI config with Effect Schema (#26952) 2026-05-11 23:51:45 +00:00
Kit Langton
fdeb2748e1 test(agent): isolate plugin agent regression (#26948) 2026-05-11 19:38:54 -04:00
Kit Langton
59e6967b8f Generate config schema from Effect Schema (#26939) 2026-05-11 19:24:58 -04:00
opencode-agent[bot]
0c619cb59c chore: generate 2026-05-11 22:34:04 +00:00
Frank
0cf90109dc zen: tps rate limit 2026-05-11 18:32:39 -04:00
Kit Langton
812668ae2f Generate TUI schema from Effect Schema (#26945) 2026-05-11 17:50:14 -04:00
Kit Langton
a5c35bf182 Avoid bootstrapping server plugins from TUI plugin runtime (#26938) 2026-05-11 17:32:16 -04:00
opencode-agent[bot]
bdd5a80886 chore: update nix node_modules hashes 2026-05-11 21:28:49 +00:00
Kit Langton
fd65d29dcc Drop unused opencode Zod statics (#26935) 2026-05-11 17:14:18 -04:00
Kit Langton
d3caac5270 chore(deps): upgrade effect to 4.0.0-beta.65 (#26934) 2026-05-11 21:09:57 +00:00
Kit Langton
fe7ca3421e Drop Config Info Zod static (#26933) 2026-05-11 16:49:22 -04:00
Kit Langton
cc95197d72 Drop prompt input Zod statics (#26923) 2026-05-11 16:49:08 -04:00
James Long
9067218b74 fix(core): always start worktrees as detached (#26931) 2026-05-11 16:22:25 -04:00
Kit Langton
42a0453945 Drop small session Zod statics (#26921) 2026-05-11 16:12:31 -04:00
Kit Langton
4d9eb6c320 Validate structured output tests with Effect Schema (#26919) 2026-05-11 16:11:25 -04:00
Kit Langton
c060c436b6 Drop LSP config Zod statics (#26920) 2026-05-11 16:11:11 -04:00
Kit Langton
45adfedd64 Drop unused domain Zod statics (#26927) 2026-05-11 16:10:58 -04:00
opencode-agent[bot]
0bced8ec96 chore: update nix node_modules hashes 2026-05-11 17:32:56 +00:00
Kit Langton
8d9b9719be Drop unused small ID Zod statics (#26908) 2026-05-11 13:22:59 -04:00
Kit Langton
c7e084c32c Simplify single-backend HttpApi exerciser (#26906) 2026-05-11 13:22:31 -04:00
Kit Langton
6f2f759fbb Clean up post-Hono references (#26903) 2026-05-11 13:20:54 -04:00
opencode-agent[bot]
cddab63808 chore: generate 2026-05-11 17:17:38 +00:00
Shoubhit Dash
12583b18f0 feat(tui): pin, quick-switch, and cycle recent sessions (#26858) 2026-05-11 22:46:27 +05:30
Kit Langton
df386bd651 feat(skill): enable customize-opencode by default, link full schema (#26899) 2026-05-11 13:13:41 -04:00
opencode-agent[bot]
25b12ed754 chore: generate 2026-05-11 17:06:57 +00:00
Kit Langton
023e1c711e refactor(llm): colocate per-type factories on their namespaces (#26799) 2026-05-11 13:05:41 -04:00
Kit Langton
52f7ba7d4d fix(llm): drop removed dispatch option from recorded cache tests (#26900) 2026-05-11 11:48:30 -04:00
opencode-agent[bot]
19fce2bc6f chore: generate 2026-05-11 15:21:20 +00:00
Shoubhit Dash
4bae84c8b0 feat(scout): autocomplete configured mentions (#26843) 2026-05-11 20:50:03 +05:30
Kit Langton
f240bba8e7 chore(http-recorder): remove content-matching dispatch mode (#26792) 2026-05-11 11:10:18 -04:00
Kit Langton
bcee247988 Define project update input with Effect Schema (#26803) 2026-05-11 15:05:31 +00:00
Kit Langton
64c504212a Parse command config with Effect Schema (#26801) 2026-05-11 10:22:45 -04:00
opencode-agent[bot]
6592d80460 chore: generate 2026-05-11 14:13:39 +00:00
Sebastian
d821650b48 add default diff parser for markdown fenced code blocks (#26887) 2026-05-11 14:12:32 +00:00
Victor Navarro
6c9f12bb2d chore: exclude status 429 from free model alerts (#26879) 2026-05-11 16:04:54 +02:00
opencode-agent[bot]
fca89e5e4d chore: generate 2026-05-11 13:44:39 +00:00
Frank
1eff01b6a5 sync 2026-05-11 09:43:12 -04:00
opencode-agent[bot]
e93b1f3a7f chore: generate 2026-05-11 13:38:44 +00:00
Frank
8874d4a42f zen: deekseek v4 flash free 2026-05-11 09:37:14 -04:00
Brendan Allan
c933504d9c fix(ui): better handle patch file support when rendering patch/edit tools (#26828) 2026-05-11 17:21:59 +08:00
Shoubhit Dash
2d0d3d596e feat(compaction): serialize compaction tail (#26830) 2026-05-11 13:40:36 +05:30
opencode-agent[bot]
3bd98ea055 chore: generate 2026-05-11 07:28:32 +00:00
Shoubhit Dash
7e997cfba4 refactor(scout): resolve configured reference mentions (#26701) 2026-05-11 12:57:26 +05:30
Brendan Allan
5d6f2a1524 fix(ui): use part_text_accum_delta to prevent markdown cutoff during streaming (#26822) 2026-05-11 15:15:03 +08:00
opencode-agent[bot]
b1cb71856e chore: generate 2026-05-11 05:27:40 +00:00
Aiden Cline
518264fcd9 fix(opencode): fix full session fork (#26811) 2026-05-11 00:26:36 -05:00
Dax
7235c9c9b8 Trace data migrations (#26809) 2026-05-11 03:23:01 +00:00
Kit Langton
274033cd52 Validate prompt messages with Effect Schema (#26796) 2026-05-11 02:59:20 +00:00
Kit Langton
9b369ee815 chore(llm): make cache: 'auto' the default (#26798) 2026-05-10 22:46:01 -04:00
Sebastian
721ff5121e fix prompt history behaviour and session line up/down commands (#26797) 2026-05-11 02:27:46 +00:00
opencode-agent[bot]
cfbf5d1c6f chore: update nix node_modules hashes 2026-05-11 02:20:35 +00:00
opencode-agent[bot]
02cb7e7b71 chore: generate 2026-05-11 02:11:07 +00:00
Kit Langton
942630eb4a feat(llm): cache-policy auto-placement (#26786) 2026-05-10 22:09:55 -04:00
opencode
ce66b191d1 sync release versions for v1.14.48 2026-05-11 02:07:48 +00:00
Kit Langton
6f1f5944ce Delete unused opencode Zod helpers (#26793) 2026-05-10 21:57:18 -04:00
opencode-agent[bot]
1c49b2ed67 chore: generate 2026-05-11 01:55:22 +00:00
opencode-agent[bot]
3c6be604ec chore: update nix node_modules hashes 2026-05-11 01:54:13 +00:00
Kit Langton
ddc02c2893 Drop synchronous SyncEvent facades (#26789) 2026-05-11 01:53:50 +00:00
Kit Langton
2703eff2e2 refactor(llm): normalize Usage as inclusive total + non-overlapping breakdown (#26735) 2026-05-10 21:52:50 -04:00
opencode-agent[bot]
38e4540119 chore: generate 2026-05-11 01:41:26 +00:00
Dax Raad
4100fcbd17 disable image resizing 2026-05-10 21:40:12 -04:00
Kit Langton
83cb0f60ec Drop EventV2 run facade (#26783) 2026-05-10 21:26:28 -04:00
Kit Langton
effd96755e Use SyncEvent service at event call sites (#26782) 2026-05-10 21:20:13 -04:00
opencode-agent[bot]
5801cce1b5 chore: generate 2026-05-11 01:18:39 +00:00
Kit Langton
77e6c0d329 feat(llm): cache hint TTL, breakpoint cap, and tool placement (#26779) 2026-05-10 21:17:38 -04:00
Kit Langton
fed716ada5 Clarify compaction test harness (#26777) 2026-05-10 20:41:21 -04:00
opencode
426d92e352 sync release versions for v1.14.47 2026-05-11 00:23:17 +00:00
opencode-agent[bot]
d0412a213b chore: generate 2026-05-11 00:18:03 +00:00
Kit Langton
16aa67086b Effectify remaining compaction process tests (#26776) 2026-05-10 20:17:01 -04:00
Dax
7cea32ee08 Add background code migration service (#26652) 2026-05-11 00:16:42 +00:00
Kit Langton
128d10d9e9 Simplify compaction test helpers (#26742) 2026-05-10 20:03:11 -04:00
Kit Langton
5ef72e1101 Drop unused ID Zod statics (#26740) 2026-05-10 19:58:21 -04:00
Kit Langton
d8060dc9ad Drop compaction create facade (#26739) 2026-05-10 19:54:16 -04:00
Kit Langton
f71fb18d3d Replace compaction create test fixtures (#26738) 2026-05-10 19:53:22 -04:00
Sebastian
5654dd2aad restore managed textarea keymap handling (#26771) 2026-05-11 01:45:59 +02:00
Kit Langton
64dde0cb15 Migrate compaction tool-call test (#26737) 2026-05-10 19:44:42 -04:00
Kit Langton
a658e43eeb Migrate compaction plugin test (#26736) 2026-05-10 19:42:44 -04:00
Kit Langton
ca8a42c973 Migrate LLM compaction tail tests (#26734) 2026-05-10 19:41:28 -04:00
Kit Langton
8f5f75db12 Migrate compaction LLM test (#26733) 2026-05-10 19:20:44 -04:00
Kit Langton
4ff0d07b1d Migrate configurable compaction tests (#26732) 2026-05-10 19:18:02 -04:00
Kit Langton
2a571b3cee Migrate compaction overflow tests (#26731) 2026-05-10 19:12:49 -04:00
Dax
0fffcdfe46 Persist session model switches outside event flag (#26765) 2026-05-10 18:53:14 -04:00
Dax
2ba2ee52e8 docs: document bun dev tmux capture (#26764) 2026-05-10 17:41:20 -04:00
Frank
56d818fc34 zen: fix reasoning token for openai compatible endpoint 2026-05-10 13:36:02 -04:00
Kit Langton
9c8da69196 Use Effect timeout in compaction test (#26728) 2026-05-10 12:45:54 -04:00
opencode-agent[bot]
a78018697c chore: generate 2026-05-10 16:44:40 +00:00
Kit Langton
e45b6ef1de refactor(http-recorder): use Schema.TaggedErrorClass for cassette errors (#26729) 2026-05-10 16:43:33 +00:00
opencode-agent[bot]
b616543ac2 chore: generate 2026-05-10 16:30:55 +00:00
Kit Langton
2bd3d9a696 refactor(http-recorder): hide cassette format behind Cassette seam (#26725) 2026-05-10 12:29:55 -04:00
Kit Langton
fa15dbc5ec Migrate compaction process tests (#26723) 2026-05-10 12:25:44 -04:00
opencode-agent[bot]
312e5c7a7c chore: generate 2026-05-10 16:22:29 +00:00
Kit Langton
049502fac6 fix(server): return diagnosable body for schema rejections (#26631) 2026-05-10 16:21:32 +00:00
opencode-agent[bot]
cc2915be16 chore: generate 2026-05-10 16:20:16 +00:00
Kit Langton
ce061bf661 Add explicit LLM stream lifecycle events (#26722) 2026-05-10 12:19:13 -04:00
Frank
3b8790e034 zen: fix usage css on mobile 2026-05-10 12:14:11 -04:00
Kit Langton
a4f3cedcdf Start effect-style compaction tests 2026-05-10 16:12:00 +00:00
opencode-agent[bot]
1c9a2eb239 chore: generate 2026-05-10 16:06:18 +00:00
Kit Langton
4fb417d3b5 feat(http-recorder): default mode to "auto" (#26719) 2026-05-10 16:05:11 +00:00
Kit Langton
11030c627b Scope boolean query overrides 2026-05-10 11:57:52 -04:00
opencode-agent[bot]
c104098a66 chore: generate 2026-05-10 15:55:49 +00:00
Kit Langton
49ee3ba85a Source diff message query pattern (#26638) 2026-05-10 11:54:54 -04:00
opencode-agent[bot]
4fc538378d chore: generate 2026-05-10 14:50:21 +00:00
Kit Langton
d28b5ad2f4 refactor(http-recorder): Redactor + Recorder seams, README (#26636) 2026-05-10 10:49:22 -04:00
opencode-agent[bot]
6589a66822 chore: generate 2026-05-10 12:28:11 +00:00
Shoubhit Dash
5cf9abe743 feat(scout): materialize configured reference repos (#26692) 2026-05-10 17:57:11 +05:30
Frank
903d81819d Zen: add Ring 2.6 1T 2026-05-10 03:51:34 -04:00
opencode-agent[bot]
472f9e64a6 chore: update nix node_modules hashes 2026-05-10 07:06:30 +00:00
Frank
c04fa9e253 sync: revert
This reverts commit 3a7f617098.
2026-05-10 02:58:46 -04:00
opencode-agent[bot]
3a78fb1f42 chore: generate 2026-05-10 06:49:21 +00:00
Aiden Cline
85ce6a5f95 feat: better image handling (auto resize & max size constraints) (#26401) 2026-05-10 01:48:19 -05:00
opencode-agent[bot]
5217e6c1af chore: generate 2026-05-10 06:39:09 +00:00
Frank
3a7f617098 go: add tencent icon 2026-05-10 02:37:50 -04:00
opencode-agent[bot]
d9150413cb chore: generate 2026-05-10 06:24:35 +00:00
Jack
bcbc1dba22 Go add hy3 preview (#26533) 2026-05-10 02:23:34 -04:00
Frank
ce3235e115 sync 2026-05-10 02:17:32 -04:00
opencode-agent[bot]
a9a2a597d5 chore: generate 2026-05-10 04:30:04 +00:00
Dax
3753601f87 Format TUI paths relative to session directory (#26648) 2026-05-10 04:29:02 +00:00
Kit Langton
fb4bab8a66 Remove redundant ID Zod overrides (#26633) 2026-05-09 23:12:21 -04:00
opencode-agent[bot]
b3526f6ce9 chore: generate 2026-05-10 03:03:37 +00:00
Kit Langton
f220f02a2f Source workspace path pattern (#26632) 2026-05-09 23:02:31 -04:00
opencode-agent[bot]
235a86fb60 chore: generate 2026-05-10 02:59:46 +00:00
Kit Langton
67b9c9c027 Source HTTP API ID path patterns (#26623) 2026-05-09 22:58:47 -04:00
opencode
2f11c9f7ed sync release versions for v1.14.46 2026-05-10 02:34:36 +00:00
opencode-agent[bot]
e1c1193f3e chore: generate 2026-05-10 02:11:45 +00:00
Kit Langton
29250a0efb fix(session): loosen remaining stored numeric schemas to tolerate legacy data (#26622) 2026-05-09 22:10:48 -04:00
Kit Langton
c6e6bdf59f fix(session): tolerate negative token counts in stored parts (#26620) 2026-05-09 22:10:44 -04:00
opencode-agent[bot]
d80e1199ca chore: generate 2026-05-10 02:06:39 +00:00
Kit Langton
10ea59066f feat(skill): built-in opencode-meta skill (#26617) 2026-05-09 22:05:37 -04:00
Kit Langton
79d6b10d7c fix(mcp): tolerate output schema ref failures (#26614) 2026-05-09 22:03:59 -04:00
Kit Langton
6e78f36a0f Narrow HTTP API numeric query overrides (#26618) 2026-05-09 22:02:51 -04:00
Kit Langton
16866e1180 Share HTTP API boolean query schema (#26615) 2026-05-09 21:41:15 -04:00
opencode-agent[bot]
6d130e5deb chore: generate 2026-05-10 01:13:41 +00:00
Kit Langton
e30d8173c1 Fix OpenAPI workspace query drift (#26609) 2026-05-09 21:12:34 -04:00
opencode
7a79f3a5ea sync release versions for v1.14.45 2026-05-10 00:07:24 +00:00
Kit Langton
b8ca71d309 fix(task): subagent inherits parent agent's deny rules (Plan Mode security bypass) (#26597)
Co-authored-by: Developer <temp@example.com>
2026-05-09 23:51:55 +00:00
Kit Langton
6849f96825 refactor(provider): share model status schema (#26595)
Co-authored-by: Developer <temp@example.com>
2026-05-09 19:20:31 -04:00
Kit Langton
00c3248295 fix(config): allow active provider model status (#26592)
Co-authored-by: Developer <temp@example.com>
2026-05-09 18:59:05 -04:00
opencode-agent[bot]
818b56dbd0 chore: generate 2026-05-09 22:47:54 +00:00
Kit Langton
29b5b24787 fix(tui): aggregate bootstrap request failures
TUI bootstrap now reports all parallel fetch failures together instead of losing sibling failures after the first rejection.
2026-05-09 18:46:52 -04:00
Kit Langton
11363170ca fix(sdk): wrap thrown error bodies in Error
SDK throwOnError paths now convert structured response bodies into real Error instances while preserving the original body and status in cause.
2026-05-09 18:46:43 -04:00
Kit Langton
ba9e4b67ed fix(tool/read): match permission patterns against worktree-relative path
Read permission checks now use the same worktree-relative path basis as edit/write/apply_patch, so configured patterns apply consistently.
2026-05-09 18:46:29 -04:00
Kit Langton
bd1029b19f test(server): cover HttpApi context inheritance
Adds regression coverage for request context inheritance in promptAsync and explicit context provisioning in stream bodies.
2026-05-09 18:46:10 -04:00
Kit Langton
43b51f09d0 fix(httpapi): align runtime query schemas with workspace routing params (#26581)
Co-authored-by: Developer <temp@example.com>
2026-05-09 16:50:30 -04:00
opencode-agent[bot]
c61ab51886 chore: generate 2026-05-09 20:45:29 +00:00
Kit Langton
d373c562f2 fix(session): accept legacy summary diffs (#26579)
Co-authored-by: Developer <temp@example.com>
2026-05-09 16:44:24 -04:00
opencode-agent[bot]
5fa5d876fc chore: generate 2026-05-09 20:31:32 +00:00
Kit Langton
805af011c9 test(session): regression test for #26574 + mirror loosening on Vcs.FileDiff (#26578)
Co-authored-by: Developer <temp@example.com>
2026-05-09 16:30:31 -04:00
opencode-agent[bot]
480aa8b23b chore: generate 2026-05-09 20:24:28 +00:00
OpeOginni
d62442bb5d fix(sessions): allow optional patch field in diff for migrated sessions (#26574) 2026-05-09 16:23:28 -04:00
Kit Langton
8602937a37 test(session): cover workspace-routed messages (#26576) 2026-05-09 16:19:06 -04:00
Kit Langton
77da433e0a fix(session): accept routing params in message list (#26569)
Co-authored-by: Developer <temp@example.com>
2026-05-09 16:02:35 -04:00
opencode-agent[bot]
ad79f3e0cf chore: generate 2026-05-09 19:59:00 +00:00
Kit Langton
6c2dfd2f52 fix(tui): guard messages.data in session.sync against undefined (#26566)
Co-authored-by: Developer <temp@example.com>
2026-05-09 19:58:03 +00:00
Sebastian
9a8b54fe62 Plugin command API shim (#26564) 2026-05-09 21:56:49 +02:00
Dax
dcdbdb218f Move schema utilities into core (#26565) 2026-05-09 19:51:09 +00:00
Kit Langton
5e49029e70 fix(provider): isolate plugin model mutations (#26561)
Co-authored-by: Developer <temp@example.com>
2026-05-09 15:47:07 -04:00
opencode
19abadaf27 sync release versions for v1.14.44 2026-05-09 19:33:26 +00:00
Kit Langton
6fea0178eb fix(server): defer validation error body change 2026-05-09 14:50:45 -04:00
Kit Langton
ebe9dcf27e fix(server): return validation error bodies 2026-05-09 14:39:25 -04:00
Kit Langton
57efec4429 fix(storage): default workspace time migration (#26556) 2026-05-09 14:18:55 -04:00
opencode
e22144806f sync release versions for v1.14.43 2026-05-09 18:06:16 +00:00
Kit Langton
27fa297a42 fix(server): keep provider lists JSON-safe (#26550) 2026-05-09 13:40:46 -04:00
Aiden Cline
b1cd25de3d ignore: fix typerrs on dev (#26544) 2026-05-09 12:08:00 -05:00
opencode
780bbb0f3b sync release versions for v1.14.42 2026-05-09 16:54:43 +00:00
Steffen Deusch
817cb076a8 fix(acp): include tool image attachments in updates (#25128) 2026-05-09 11:45:44 -05:00
opencode-agent[bot]
347526e9fd chore: generate 2026-05-09 16:32:52 +00:00
Polo123456789
092bc674a5 fix(sidebar): fix logic and missleading message #26469 (#26470) 2026-05-09 11:31:46 -05:00
Kit Langton
b24a4e897e chore(server): clean up post-Hono-deletion scar tissue (#26542) 2026-05-09 12:30:18 -04:00
opencode-agent[bot]
3afa622eab chore: update nix node_modules hashes 2026-05-09 13:22:38 +00:00
opencode-agent[bot]
d01cb7f013 chore: generate 2026-05-09 13:11:45 +00:00
Kit Langton
28b03595bf research: delete Hono backend (do not merge) (#25667) 2026-05-09 13:10:42 +00:00
Kit Langton
32684e70e6 test(server): expect null body from HTTP API authorize() with no redirect (#26515) 2026-05-09 08:50:00 -04:00
opencode-agent[bot]
b2baddcd37 chore: generate 2026-05-09 04:31:07 +00:00
Kit Langton
dbd48d423d fix(server): match Hono wire format for authorize undefined and share errors (#26474) 2026-05-09 00:30:00 -04:00
Kit Langton
e7cc8259b5 test(server): drop flaky account error-mapping test (#26475) 2026-05-09 00:29:31 -04:00
opencode-agent[bot]
a9ccb0804f chore: generate 2026-05-09 04:22:25 +00:00
Kit Langton
ebe6087e8f fix(server): return structured validation errors (#26457) 2026-05-09 00:21:19 -04:00
Kit Langton
dc978cb889 fix(server): validate permission and question ids (#26456) 2026-05-09 00:20:28 -04:00
Kit Langton
8cbc43fbb0 fix(server): include auth challenge on typed 401 (#26455) 2026-05-09 00:15:20 -04:00
opencode-agent[bot]
82359c4b1b chore: generate 2026-05-09 04:06:08 +00:00
Kit Langton
7f3e51453b test(server): use Layer.mock for partial Account service stub (#26472) 2026-05-09 00:05:05 -04:00
Kit Langton
cbdb2d9825 test(server): expand workspace routing fixed-id coverage (#26458) 2026-05-09 00:00:18 -04:00
Kit Langton
96bde05f6e docs(server): explain why HTTP API PTY handler has no early-frame buffer (#26464) 2026-05-08 23:58:33 -04:00
Kit Langton
dcb8ed8eb0 test(server): cover workspace sync fence protocol (#26441) 2026-05-08 23:55:47 -04:00
Kit Langton
aab82cc1a7 test(project): rescue non-Hono InstanceBootstrap boundary tests (#26453) 2026-05-08 23:53:42 -04:00
Kit Langton
cd1d1e81a6 test(server): run httpapi exercise effect mode in test:httpapi (#26452) 2026-05-08 23:48:29 -04:00
opencode-agent[bot]
cff441909a chore: generate 2026-05-09 03:45:35 +00:00
Kit Langton
3615d5aab1 fix(server): map Account failures to typed 500 instead of defect (#26448) 2026-05-08 23:44:28 -04:00
opencode-agent[bot]
11d9e82eaf chore: generate 2026-05-09 03:42:23 +00:00
Kit Langton
eadda11ec9 refactor(server): use JSON response for OpenAPI doc route (#26447) 2026-05-08 23:41:20 -04:00
Kit Langton
f73a56c223 fix(server): log instance disposal failures from HTTP API lifecycle (#26446) 2026-05-08 23:40:06 -04:00
opencode-agent[bot]
c0acf5c43f chore: generate 2026-05-09 03:18:57 +00:00
Kit Langton
4d585464f3 fix(server): include Origin in CORS preflight Vary header (#26445) 2026-05-08 23:17:47 -04:00
Jose Vargas
f0cb17a812 fix(tui): sort session picker by full updated timestamp (#24725)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-05-08 22:17:13 -05:00
Kit Langton
357a74714a fix(test): set OPENCODE_EXPERIMENTAL_WORKSPACES in fence header test (#26466) 2026-05-08 23:12:08 -04:00
Kit Langton
ffea6c7974 feat(server): add HTTP API response compression (#26440) 2026-05-08 23:06:00 -04:00
Kit Langton
8e9550d90d fix(server): emit fixed workspace fence headers (#26443) 2026-05-08 22:45:54 -04:00
James Long
9b7b6cb30f feat(core): be smarter about generating a worktree name (#26368) 2026-05-08 22:45:44 -04:00
Kit Langton
cc68afb2de test(server): lock fixed workspace routing context (#26454) 2026-05-08 22:35:32 -04:00
Kit Langton
11c33d52a5 test(server): cover REST API project skills (#26451) 2026-05-09 02:23:21 +00:00
opencode-agent[bot]
2a487ea990 chore: generate 2026-05-09 02:21:54 +00:00
Kit Langton
9c05d4e2fd fix(server): serve HttpApi OpenAPI document (#26438) 2026-05-08 22:20:50 -04:00
Kit Langton
0745162eab test(server): harden HttpApi exercise coverage (#26425) 2026-05-08 20:50:01 -04:00
Kit Langton
21d055be19 fix(workspace): claim detached sessions to source project (#26413) 2026-05-08 20:25:53 -04:00
Luke Parker
d19f7bc77c fix(web): normalize shell output carriage returns (#26426) 2026-05-09 10:24:57 +10:00
opencode-agent[bot]
8694da9309 chore: update nix node_modules hashes 2026-05-08 23:46:19 +00:00
Sebastian
a0fc27e424 flatten to keybind compatible config (#26421) 2026-05-09 01:29:13 +02:00
opencode-agent[bot]
35deef6175 chore: generate 2026-05-08 21:19:51 +00:00
vimtor
36f8b7e47d chore: reduce alerts false positives 2026-05-08 23:18:04 +02:00
opencode-agent[bot]
f4337dff3c chore: update nix node_modules hashes 2026-05-08 21:11:02 +00:00
opencode-agent[bot]
ba8c920639 chore: generate 2026-05-08 20:57:36 +00:00
Kit Langton
5bb7b23440 Add native LLM core foundation (#24712) 2026-05-08 16:56:20 -04:00
opencode-agent[bot]
dc7d665e94 chore: generate 2026-05-08 20:22:22 +00:00
Shoubhit Dash
40d5ea1cf1 feat(core): add scout agent for repo research (#24149)
Co-authored-by: Dax Raad <d@ironbay.co>
2026-05-08 20:20:08 +00:00
Chris Huber
6e47ae769e fix(cli): forward signals from npm shim (#26259)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-05-08 14:00:51 -05:00
opencode-agent[bot]
9ca4be62bd chore: generate 2026-05-08 18:51:43 +00:00
Dax
fed221e0b0 fix(skill): allow missing descriptions (#26391) 2026-05-08 18:50:06 +00:00
Kit Langton
75308ea47d test(server): add HttpApi auth exercise mode (#26386) 2026-05-08 14:05:46 -04:00
Kit Langton
daa3116f4b refactor(server): split HttpApi exercise harness (#26385) 2026-05-08 17:58:14 +00:00
opencode-agent[bot]
9e7f7bf8e4 chore: generate 2026-05-08 17:46:57 +00:00
Simon Klee
4d43d584fe cli/run: switch to global event stream (#26383) 2026-05-08 19:44:31 +02:00
Kit Langton
3052a79b32 refactor(server): clarify HttpApi route auth layers (#26372) 2026-05-08 13:06:00 -04:00
Rajvardhan Patil
13b3117ca9 fix(server): require auth for effect root routes (#26361)
Co-authored-by: Rajvardhan Patil <243567420+RajvardhanPatil07@users.noreply.github.com>
2026-05-08 16:39:11 +00:00
Aiden Cline
83bb216486 fix: ensure tools are always in same order (#26370) 2026-05-08 10:59:41 -05:00
opencode-agent[bot]
fc46cef5fd chore: generate 2026-05-08 15:49:37 +00:00
Aiden Cline
799996db76 fix: adjust tui retry dialog logic to be more provider specific and error case specific (#26366) 2026-05-08 10:48:19 -05:00
opencode-agent[bot]
df75bfe07c chore: generate 2026-05-08 15:10:32 +00:00
James Long
c818c9dcb6 feat(core): allow external workspace creation (#26212) 2026-05-08 11:09:12 -04:00
Kit Langton
c36ab3f935 fix(provider): align Gemini thinking controls (#26279) 2026-05-08 10:22:45 -04:00
vimtor
e3c983c21f chore: reduce provider alerts query frequency 2026-05-08 14:00:24 +02:00
opencode-agent[bot]
a196569f51 chore: generate 2026-05-08 11:56:55 +00:00
Sebastian
19da27e1cb internal which-key plugin, inactive by default (#26337) 2026-05-08 13:55:49 +02:00
Frank
4a737493ac Revert "zen: update tpm rate limit algo"
This reverts commit 6869186fc6.
2026-05-08 06:31:12 -04:00
opencode-agent[bot]
ec301f6a2c chore: update nix node_modules hashes 2026-05-08 10:31:03 +00:00
opencode-agent[bot]
ac7a885ae9 chore: generate 2026-05-08 10:18:22 +00:00
Simon Klee
7f2b5ee8c2 feat(opencode): add interactive split-footer mode to run (#23557) 2026-05-08 12:17:14 +02:00
vimtor
15784aa036 chore: reduce alerts threshold 2026-05-08 12:06:39 +02:00
opencode-agent[bot]
edbc02855d chore: generate 2026-05-08 08:50:42 +00:00
Shoubhit Dash
a43d3e0e1e feat(websearch): add parallel provider rollout (#26227) 2026-05-08 14:19:36 +05:30
Shoubhit Dash
ae25278eda test(session): update go retry fixture (#26312) 2026-05-08 14:10:18 +05:30
Frank
6869186fc6 zen: update tpm rate limit algo 2026-05-08 03:52:59 -04:00
Frank
f8c6742e54 zen: lift default rate limit 2026-05-08 03:52:59 -04:00
opencode-agent[bot]
014dbd34c4 chore: generate 2026-05-08 06:20:21 +00:00
Brendan Allan
21ae91b4f2 refactor(desktop): convert main process to Effect-TS (#26148) 2026-05-08 14:19:09 +08:00
Frank
bb3f14119b tui: update go upsell copy 2026-05-08 01:51:33 -04:00
Luke Parker
6f165e23de perf(ui): defer tool status width measurement (#26282) 2026-05-08 15:36:28 +10:00
opencode-agent[bot]
cef0c8ac84 chore: generate 2026-05-08 05:36:06 +00:00
Brendan Allan
dd8bb44d1d refactor(desktop): use electron-log in shell-env and simplify env merging (#26284) 2026-05-08 13:34:53 +08:00
Frank
30868f52ea go: update rate limit error copy 2026-05-08 00:27:28 -04:00
opencode-agent[bot]
9c88235121 chore: generate 2026-05-08 04:18:54 +00:00
Aiden Cline
4e14f79511 fix: tweaks to transform logic for anthropic and bedrock (#26276) 2026-05-07 23:17:43 -05:00
Kit Langton
e0396b809a fix(provider): align Anthropic Opus 4.5 efforts (#26275) 2026-05-08 00:06:07 -04:00
Kit Langton
319498e2fd fix(provider): constrain OpenAI deep research efforts (#26273) 2026-05-07 23:43:42 -04:00
Erik Demaine
2ba9aa2196 feat(desktop): working indicator on project sidebar (#26223) 2026-05-08 11:42:39 +08:00
opencode-agent[bot]
114eeb21dc chore: generate 2026-05-08 03:33:55 +00:00
Kit Langton
1cf8123bc6 fix(provider): align GPT-5 reasoning variants (#26268) 2026-05-07 23:32:47 -04:00
Frank
db6a038829 sync 2026-05-07 22:55:50 -04:00
opencode-agent[bot]
6ff833a22b chore: generate 2026-05-08 02:40:47 +00:00
Sebastian
5c401673b2 improve go sub animation perf (#26251) 2026-05-08 04:39:42 +02:00
Kit Langton
e8ce5df414 fix(tui): retain cleared prompt drafts (#26258) 2026-05-08 02:08:29 +00:00
Luke Parker
b8799be3c8 feat(desktop): allow silent install and only user-wide scope (#26253) 2026-05-07 23:58:16 +00:00
opencode-agent[bot]
7ded0ec9e9 chore: generate 2026-05-07 21:54:31 +00:00
Aiden Cline
f5d0371efe tui: go plan payg msg (#26248) 2026-05-07 16:53:24 -05:00
Frank
22e64cac67 sync: cleanup 2026-05-07 15:27:36 -04:00
vimtor
2a1305f231 chore: increase alerting threshold 2026-05-07 21:07:26 +02:00
opencode-agent[bot]
e691e8f274 chore: update nix node_modules hashes 2026-05-07 18:55:24 +00:00
vimtor
cc6dd5321c chore: improve variant parsing for zen 2026-05-07 20:38:14 +02:00
opencode-agent[bot]
2c17e3a4db chore: generate 2026-05-07 18:36:42 +00:00
Sebastian
98f5e6e713 introduce opentui keymap as sole key/cmd engine (#26053) 2026-05-07 20:35:31 +02:00
Frank
474e311f6f sync 2026-05-07 14:23:23 -04:00
Victor Navarro
626a488fb8 chore: track model variant in honeycomb (#26188) 2026-05-07 19:38:04 +02:00
Dax Raad
a300a6cc7a rebase migrations properly 2026-05-07 12:38:29 -04:00
opencode
fe594693a4 sync release versions for v1.14.41 2026-05-07 14:52:09 +00:00
opencode-agent[bot]
98e091796b chore: generate 2026-05-07 14:25:30 +00:00
James Long
3c4b4d5faf feat(core): copy file changes when warping (#26190) 2026-05-07 10:24:17 -04:00
vimtor
b6ff1b18c7 chore: activate free tier requests query 2026-05-07 14:10:47 +02:00
vimtor
9c9bc09f52 chore: fix free tier query 2026-05-07 13:58:41 +02:00
vimtor
d6e06c8950 chore: fix free tier query 2026-05-07 13:41:14 +02:00
opencode-agent[bot]
844fb71938 chore: generate 2026-05-07 11:37:34 +00:00
vimtor
fbb7b5b1bf chore: add free tier usage alert 2026-05-07 13:36:18 +02:00
Shoubhit Dash
95280ebec9 fix(tui): restore custom provider in /connect (#26168) 2026-05-07 17:05:35 +05:30
YGoetschel
fea9a0bd4c fix: guard undefined contents in diff renderer to fix share viewer SSR crash (#21763) 2026-05-07 18:55:40 +08:00
vimtor
30c4fcb1a5 chore: fix honeycomb query frequency 2026-05-07 11:10:36 +02:00
vimtor
193c169ca5 chore: improve provider down query 2026-05-07 11:00:43 +02:00
vimtor
cee04f2924 chore: make provider down queries live 2026-05-07 10:56:37 +02:00
Brendan Allan
1219691c11 docs(desktop): update README from Tauri to Electron (#26146) 2026-05-07 16:31:37 +08:00
vimtor
b2cc40f09c chore: first provider alert version 2026-05-07 10:30:29 +02:00
vimtor
0b2e65f16d chore: reactivate alerts 2026-05-07 10:15:52 +02:00
opencode-agent[bot]
1ea01fdad0 chore: update nix node_modules hashes 2026-05-07 08:08:37 +00:00
Victor Navarro
f8aa4a3be0 chore: simplify honeycomb alerts (#26142) 2026-05-07 07:56:10 +00:00
Bence Ferdinandy
293bb422fa fix(format): restore stdout/stderr ignore for formatter processes (#26037)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-05-07 00:52:07 -05:00
Brendan Allan
54a78c9224 feat(desktop): move server to utilityProcess (#25962)
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
2026-05-07 13:48:56 +08:00
Jesse
9b30ee2db2 fix(desktop): add macOS settings menu entry (#26081)
Co-authored-by: jesse.mahnken <jesse.mahnken@tiefox.de>
2026-05-07 13:39:14 +08:00
carmit hershman
ba1ec62caf docs: add opencode-jfrog-plugin to ecosystem list for JFrog integration (#26019) 2026-05-07 00:37:14 -05:00
Frank
72ec05d0be go: rate limit metadata 2026-05-07 00:32:33 -04:00
Frank
0b702704ae zen: nano not used for title gen 2026-05-06 23:01:16 -04:00
opencode-agent[bot]
3480cef52e chore: update nix node_modules hashes 2026-05-07 00:46:33 +00:00
opencode
dcfe4b0d51 sync release versions for v1.14.40 2026-05-07 00:34:09 +00:00
Aiden Cline
b2e3dc87ea feat: Update ACP support, modernize and fix misc issues (#25663) 2026-05-06 19:33:52 -05:00
1211 changed files with 300715 additions and 105853 deletions

View File

@@ -23,7 +23,7 @@ runs:
fi
- name: Setup Bun
uses: oven-sh/setup-bun@v2
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version-file: ${{ !steps.bun-url.outputs.url && 'package.json' || '' }}
bun-download-url: ${{ steps.bun-url.outputs.url }}
@@ -33,8 +33,9 @@ runs:
shell: bash
run: echo "dir=$(bun pm cache)" >> "$GITHUB_OUTPUT"
- name: Cache Bun dependencies
uses: actions/cache@v4
- name: Restore Bun dependencies
id: bun-cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ${{ steps.cache.outputs.dir }}
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
@@ -56,3 +57,10 @@ runs:
bun install ${{ inputs.install-flags }}
fi
shell: bash
- name: Save Bun dependencies
if: steps.bun-cache.outputs.cache-hit != 'true' && github.event_name != 'pull_request' && github.event_name != 'pull_request_target'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ${{ steps.cache.outputs.dir }}
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}

View File

@@ -19,7 +19,7 @@ runs:
steps:
- name: Create app token
id: apptoken
uses: actions/create-github-app-token@v2
uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2.2.2
with:
app-id: ${{ inputs.opencode-app-id }}
private-key: ${{ inputs.opencode-app-secret }}

View File

@@ -13,7 +13,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0

View File

@@ -12,9 +12,9 @@ jobs:
contents: read
issues: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: oven-sh/setup-bun@v2
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: latest

View File

@@ -21,7 +21,7 @@ jobs:
timeout-minutes: 15
steps:
- name: Close inactive PRs
uses: actions/github-script@v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Close non-compliant issues and PRs after 2 hours
uses: actions/github-script@v7
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const { data: items } = await github.rest.issues.listForRepo({

View File

@@ -21,18 +21,18 @@ jobs:
REGISTRY: ghcr.io/${{ github.repository_owner }}
TAG: "24.04"
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-bun
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Login to GHCR
uses: docker/login-action@v3
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}

View File

@@ -13,11 +13,11 @@ jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ./.github/actions/setup-bun
- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "24"
@@ -37,7 +37,6 @@ jobs:
PLANETSCALE_SERVICE_TOKEN: ${{ secrets.PLANETSCALE_SERVICE_TOKEN }}
STRIPE_SECRET_KEY: ${{ github.ref_name == 'production' && secrets.STRIPE_SECRET_KEY_PROD || secrets.STRIPE_SECRET_KEY_DEV }}
HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY }}
INCIDENT_API_KEY: ${{ secrets.INCIDENT_API_KEY }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
SENTRY_PROJECT: ${{ vars.WEB_SENTRY_PROJECT }}

View File

@@ -16,7 +16,7 @@ jobs:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
fetch-depth: 0

View File

@@ -18,7 +18,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0 # Fetch full history to access commits
@@ -43,7 +43,7 @@ jobs:
- name: Run opencode
if: steps.commits.outputs.has_commits == 'true'
uses: sst/opencode/github@latest
uses: sst/opencode/github@2c14fc5586fe0b88e5c04732d2e846769cc35671 # latest
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
with:

View File

@@ -13,7 +13,7 @@ jobs:
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 1
@@ -125,7 +125,7 @@ jobs:
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 1

View File

@@ -13,7 +13,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Setup Bun
uses: ./.github/actions/setup-bun

View File

@@ -20,10 +20,10 @@ jobs:
timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Nix
uses: nixbuild/nix-quick-install-action@v34
uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
- name: Evaluate flake outputs (all systems)
run: |

View File

@@ -41,10 +41,10 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Nix
uses: nixbuild/nix-quick-install-action@v34
uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
- name: Compute node_modules hash
id: hash
@@ -72,7 +72,7 @@ jobs:
echo "Computed hash for ${SYSTEM}: $HASH"
- name: Upload hash
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: hash-${{ matrix.system }}
path: hash.txt
@@ -85,7 +85,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
fetch-depth: 0
@@ -102,7 +102,7 @@ jobs:
git pull --rebase --autostash origin "$GITHUB_REF_NAME"
- name: Download hash artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
path: hashes
pattern: hash-*

View File

@@ -9,6 +9,6 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Send nicely-formatted embed to Discord
uses: SethCohen/github-releases-to-discord@v1
uses: SethCohen/github-releases-to-discord@24d166886aee4646d448c8a389ff9e1ebcab3682 # v1.20.0
with:
webhook_url: ${{ secrets.DISCORD_WEBHOOK }}

View File

@@ -21,12 +21,12 @@ jobs:
issues: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: ./.github/actions/setup-bun
- name: Run opencode
uses: anomalyco/opencode/github@latest
uses: anomalyco/opencode/github@2c14fc5586fe0b88e5c04732d2e846769cc35671 # latest
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
OPENCODE_PERMISSION: '{"bash": "deny"}'

View File

@@ -12,7 +12,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 1
@@ -78,7 +78,7 @@ jobs:
issues: write
steps:
- name: Add Contributor Label
uses: actions/github-script@v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const isPR = !!context.payload.pull_request;

View File

@@ -12,7 +12,7 @@ jobs:
pull-requests: write
steps:
- name: Check PR standards
uses: actions/github-script@v7
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const pr = context.payload.pull_request;
@@ -159,7 +159,7 @@ jobs:
pull-requests: write
steps:
- name: Check PR template compliance
uses: actions/github-script@v7
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const pr = context.payload.pull_request;

View File

@@ -16,7 +16,7 @@ jobs:
publish:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
with:
fetch-depth: 0

View File

@@ -15,7 +15,7 @@ jobs:
publish:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
with:
fetch-depth: 0

View File

@@ -35,7 +35,7 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.repository == 'anomalyco/opencode'
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
with:
fetch-depth: 0
@@ -72,7 +72,7 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.repository == 'anomalyco/opencode'
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
with:
fetch-tags: true
@@ -95,14 +95,14 @@ jobs:
GH_REPO: ${{ needs.version.outputs.repo }}
GH_TOKEN: ${{ steps.committer.outputs.token }}
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: opencode-cli
path: |
packages/opencode/dist/opencode-darwin*
packages/opencode/dist/opencode-linux*
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: opencode-cli-windows
path: packages/opencode/dist/opencode-windows*
@@ -123,9 +123,9 @@ jobs:
AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: opencode-cli-windows
path: packages/opencode/dist
@@ -138,13 +138,13 @@ jobs:
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- name: Azure login
uses: azure/login@v2
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ env.AZURE_CLIENT_ID }}
tenant-id: ${{ env.AZURE_TENANT_ID }}
subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}
- uses: azure/artifact-signing-action@v1
- uses: azure/artifact-signing-action@b443cf8ea4124818d2ea9f043cba29fc3ec47b16 # v1.2.0
with:
endpoint: ${{ env.AZURE_TRUSTED_SIGNING_ENDPOINT }}
signing-account-name: ${{ env.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
@@ -201,7 +201,7 @@ jobs:
--clobber `
--repo "${{ needs.version.outputs.repo }}"
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: opencode-cli-signed-windows
path: |
@@ -244,14 +244,14 @@ jobs:
- host: "blacksmith-4vcpu-ubuntu-2404"
target: x86_64-unknown-linux-gnu
platform_flag: --linux
- host: "blacksmith-4vcpu-ubuntu-2404"
- host: "blacksmith-4vcpu-ubuntu-2404-arm"
target: aarch64-unknown-linux-gnu
platform_flag: --linux
platform_flag: --linux --arm64
runs-on: ${{ matrix.settings.host }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: apple-actions/import-codesign-certs@v2
- uses: apple-actions/import-codesign-certs@8f3fb608891dd2244cdab3d69cd68c0d37a7fe93 # v2.0.0
if: runner.os == 'macOS'
with:
keychain: build
@@ -268,19 +268,19 @@ jobs:
- name: Azure login
if: runner.os == 'Windows'
uses: azure/login@v2
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ env.AZURE_CLIENT_ID }}
tenant-id: ${{ env.AZURE_TENANT_ID }}
subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}
- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "24"
- name: Cache apt packages
if: contains(matrix.settings.host, 'ubuntu')
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/apt-cache
key: ${{ runner.os }}-${{ matrix.settings.target }}-apt-electron-${{ hashFiles('.github/workflows/publish.yml') }}
@@ -388,12 +388,12 @@ jobs:
}
}
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: opencode-desktop-${{ matrix.settings.target }}
path: packages/desktop/dist/*
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: needs.version.outputs.release
with:
name: latest-yml-${{ matrix.settings.target }}
@@ -408,44 +408,44 @@ jobs:
if: always() && !failure() && !cancelled()
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ./.github/actions/setup-bun
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: opencode-cli
path: packages/opencode/dist
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: opencode-cli-windows
path: packages/opencode/dist
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: opencode-cli-signed-windows
path: packages/opencode/dist
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
if: needs.version.outputs.release
with:
pattern: latest-yml-*
@@ -459,7 +459,7 @@ jobs:
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
- name: Cache apt packages (AUR)
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: /var/cache/apt/archives
key: ${{ runner.os }}-apt-aur-${{ hashFiles('.github/workflows/publish.yml') }}

View File

@@ -16,7 +16,7 @@ jobs:
release:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0

View File

@@ -25,7 +25,7 @@ jobs:
fi
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 1

View File

@@ -16,7 +16,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Setup Bun
uses: ./.github/actions/setup-bun

View File

@@ -29,7 +29,7 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Setup Bun
uses: ./.github/actions/setup-bun

View File

@@ -10,7 +10,7 @@ jobs:
name: Release Zed Extension
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0

View File

@@ -37,12 +37,12 @@ jobs:
shell: bash
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "24"
@@ -55,7 +55,7 @@ jobs:
git config --global user.name "opencode"
- name: Cache Turbo
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: node_modules/.cache/turbo
key: turbo-${{ runner.os }}-${{ hashFiles('turbo.json', '**/package.json') }}-${{ github.sha }}
@@ -68,9 +68,14 @@ jobs:
env:
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }}
- name: Run HttpApi exerciser gates
if: runner.os == 'Linux'
working-directory: packages/opencode
run: bun run test:httpapi
- name: Publish unit reports
if: always()
uses: mikepenz/action-junit-report@v6
uses: mikepenz/action-junit-report@bccf2e31636835cf0874589931c4116687171386 # v6.4.0
with:
report_paths: packages/*/.artifacts/unit/junit.xml
check_name: "unit results (${{ matrix.settings.name }})"
@@ -80,7 +85,7 @@ jobs:
- name: Upload unit artifacts
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: unit-${{ matrix.settings.name }}-${{ github.run_attempt }}
include-hidden-files: true
@@ -106,12 +111,12 @@ jobs:
shell: bash
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "24"
@@ -126,7 +131,7 @@ jobs:
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ${{ github.workspace }}/.playwright-browsers
key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright-version.outputs.version }}-chromium
@@ -150,7 +155,7 @@ jobs:
- name: Upload Playwright artifacts
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: playwright-${{ matrix.settings.name }}-${{ github.run_attempt }}
if-no-files-found: ignore

View File

@@ -12,7 +12,7 @@ jobs:
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 1

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Setup Bun
uses: ./.github/actions/setup-bun

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@ node_modules
.worktrees
.sst
.env
.env.local
.idea
.vscode
.codex

5
.gitleaksignore Normal file
View File

@@ -0,0 +1,5 @@
# Fake secret-looking strings used by HTTP recorder redaction tests.
afa57acfda894e0ebf3c637dd710310b705c0a2f:packages/http-recorder/test/record-replay.test.ts:generic-api-key:69
afa57acfda894e0ebf3c637dd710310b705c0a2f:packages/http-recorder/test/record-replay.test.ts:generic-api-key:92
afa57acfda894e0ebf3c637dd710310b705c0a2f:packages/http-recorder/test/record-replay.test.ts:generic-api-key:146
afa57acfda894e0ebf3c637dd710310b705c0a2f:packages/http-recorder/test/record-replay.test.ts:gcp-api-key:71

View File

@@ -1,11 +1,7 @@
{
"$schema": "https://opencode.ai/config.json",
"provider": {},
"permission": {
"edit": {
"packages/opencode/migration/*": "deny",
},
},
"permission": {},
"mcp": {},
"tools": {
"github-triage": false,

View File

@@ -1,35 +1,62 @@
/** @jsxImportSource @opentui/solid */
import { useKeyboard, useTerminalDimensions, type JSX } from "@opentui/solid"
import { RGBA, VignetteEffect } from "@opentui/core"
import type {
TuiKeybindSet,
TuiPlugin,
TuiPluginApi,
TuiPluginMeta,
TuiPluginModule,
TuiSlotPlugin,
} from "@opencode-ai/plugin/tui"
import { useTerminalDimensions, type JSX } from "@opentui/solid"
import { useBindings, useKeymapSelector } from "@opentui/keymap/solid"
import { RGBA, VignetteEffect, type KeyEvent, type Renderable } from "@opentui/core"
import { createBindingLookup, type BindingConfig } from "@opentui/keymap/extras"
import type { TuiPlugin, TuiPluginApi, TuiPluginMeta, TuiPluginModule, TuiSlotPlugin } from "@opencode-ai/plugin/tui"
const tabs = ["overview", "counter", "help"]
const bind = {
modal: "ctrl+shift+m",
screen: "ctrl+shift+o",
home: "escape,ctrl+h",
left: "left,h",
right: "right,l",
up: "up,k",
down: "down,j",
alert: "a",
confirm: "c",
prompt: "p",
select: "s",
modal_accept: "enter,return",
modal_close: "escape",
dialog_close: "escape",
local: "x",
local_push: "enter,return",
local_close: "q,backspace",
host: "z",
const command = {
modal: "smoke_modal",
screen: "smoke_screen",
alert: "smoke_alert",
confirm: "smoke_confirm",
prompt: "smoke_prompt",
select: "smoke_select",
host: "smoke_host",
home: "smoke_home",
toast: "smoke_toast",
dialog_close: "smoke_dialog_close",
local_push: "smoke_local_push",
local_pop: "smoke_local_pop",
screen_home: "smoke_screen_home",
screen_left: "smoke_screen_left",
screen_right: "smoke_screen_right",
screen_up: "smoke_screen_up",
screen_down: "smoke_screen_down",
screen_modal: "smoke_screen_modal",
screen_local: "smoke_screen_local",
screen_host: "smoke_screen_host",
screen_alert: "smoke_screen_alert",
screen_confirm: "smoke_screen_confirm",
screen_prompt: "smoke_screen_prompt",
screen_select: "smoke_screen_select",
modal_accept: "smoke_modal_accept",
modal_close: "smoke_modal_close",
}
type SmokeBindings = BindingConfig<Renderable, KeyEvent>
const defaultKeymap = {
[command.modal]: "ctrl+shift+m",
[command.screen]: "ctrl+shift+o",
[command.dialog_close]: "escape",
[command.local_push]: "enter,return",
[command.local_pop]: "escape,q,backspace",
[command.screen_home]: "escape,ctrl+h",
[command.screen_left]: "left,h",
[command.screen_right]: "right,l",
[command.screen_up]: "up,k",
[command.screen_down]: "down,j",
[command.screen_modal]: "ctrl+shift+m",
[command.screen_local]: "x",
[command.screen_host]: "z",
[command.screen_alert]: "a",
[command.screen_confirm]: "c",
[command.screen_prompt]: "p",
[command.screen_select]: "s",
[command.modal_accept]: "enter,return",
[command.modal_close]: "escape",
}
const pick = (value: unknown, fallback: string) => {
@@ -43,16 +70,14 @@ const num = (value: unknown, fallback: number) => {
return value
}
const rec = (value: unknown) => {
if (!value || typeof value !== "object" || Array.isArray(value)) return
return Object.fromEntries(Object.entries(value))
}
const record = (value: unknown): value is Record<string, unknown> =>
!!value && typeof value === "object" && !Array.isArray(value)
type Cfg = {
label: string
route: string
vignette: number
keybinds: Record<string, unknown> | undefined
keybinds: SmokeBindings | undefined
}
type Route = {
@@ -74,7 +99,7 @@ const cfg = (options: Record<string, unknown> | undefined) => {
label: pick(options?.label, "smoke"),
route: pick(options?.route, "workspace-smoke"),
vignette: Math.max(0, num(options?.vignette, 0.35)),
keybinds: rec(options?.keybinds),
keybinds: record(options?.keybinds) ? (options.keybinds as SmokeBindings) : undefined,
}
}
@@ -85,7 +110,12 @@ const names = (input: Cfg) => {
}
}
type Keys = TuiKeybindSet
function createKeys(input: SmokeBindings | undefined) {
return createBindingLookup({ ...defaultKeymap, ...input })
}
type Keys = ReturnType<typeof createKeys>
const ui = {
panel: "#1d1d1d",
border: "#4a4a4a",
@@ -292,125 +322,174 @@ const Screen = (props: {
}
const pop = (base?: State) => {
const next = base ?? current(props.api, props.route)
const local = Math.max(0, next.local - 1)
set(local, next)
set(Math.max(0, next.local - 1), next)
}
const show = () => {
setTimeout(() => {
open()
}, 0)
}
useKeyboard((evt) => {
if (props.api.route.current.name !== props.route.screen) return
const next = current(props.api, props.route)
if (props.api.ui.dialog.open) {
if (props.keys.match("dialog_close", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.ui.dialog.clear()
return
}
return
}
const screenActive = () => props.api.route.current.name === props.route.screen
if (next.local > 0) {
if (evt.name === "escape" || props.keys.match("local_close", evt)) {
evt.preventDefault()
evt.stopPropagation()
pop(next)
return
}
useBindings(() => ({
enabled: () => screenActive() && props.api.ui.dialog.open,
commands: [
{
name: command.dialog_close,
run() {
props.api.ui.dialog.clear()
},
},
],
bindings: props.keys.gather("smoke.dialog", [command.dialog_close]),
}))
if (props.keys.match("local_push", evt)) {
evt.preventDefault()
evt.stopPropagation()
push(next)
return
}
return
}
useBindings(() => ({
enabled: () => screenActive() && !props.api.ui.dialog.open && current(props.api, props.route).local > 0,
commands: [
{
name: command.local_push,
run() {
push(current(props.api, props.route))
},
},
{
name: command.local_pop,
run() {
pop(current(props.api, props.route))
},
},
],
bindings: props.keys.gather("smoke.local", [command.local_push, command.local_pop]),
}))
if (props.keys.match("home", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.route.navigate("home")
return
}
useBindings(() => ({
enabled: () => screenActive() && !props.api.ui.dialog.open && current(props.api, props.route).local === 0,
commands: [
{
name: command.screen_home,
run() {
props.api.route.navigate("home")
},
},
{
name: command.screen_left,
run() {
const next = current(props.api, props.route)
props.api.route.navigate(props.route.screen, { ...next, tab: (next.tab - 1 + tabs.length) % tabs.length })
},
},
{
name: command.screen_right,
run() {
const next = current(props.api, props.route)
props.api.route.navigate(props.route.screen, { ...next, tab: (next.tab + 1) % tabs.length })
},
},
{
name: command.screen_up,
run() {
const next = current(props.api, props.route)
props.api.route.navigate(props.route.screen, { ...next, count: next.count + 1 })
},
},
{
name: command.screen_down,
run() {
const next = current(props.api, props.route)
props.api.route.navigate(props.route.screen, { ...next, count: next.count - 1 })
},
},
{
name: command.screen_modal,
run() {
props.api.route.navigate(props.route.modal, current(props.api, props.route))
},
},
{
name: command.screen_local,
run() {
open()
},
},
{
name: command.screen_host,
run() {
host(props.api, props.input, skin)
},
},
{
name: command.screen_alert,
run() {
warn(props.api, props.route, current(props.api, props.route))
},
},
{
name: command.screen_confirm,
run() {
check(props.api, props.route, current(props.api, props.route))
},
},
{
name: command.screen_prompt,
run() {
entry(props.api, props.route, current(props.api, props.route))
},
},
{
name: command.screen_select,
run() {
picker(props.api, props.route, current(props.api, props.route))
},
},
],
bindings: props.keys.gather("smoke.screen", [
command.screen_home,
command.screen_left,
command.screen_right,
command.screen_up,
command.screen_down,
command.screen_modal,
command.screen_local,
command.screen_host,
command.screen_alert,
command.screen_confirm,
command.screen_prompt,
command.screen_select,
]),
}))
const shortcuts = useKeymapSelector((keymap) => {
const bindings = keymap.getCommandBindings({
visibility: "registered",
commands: [
command.screen_home,
command.screen_up,
command.screen_down,
command.screen_modal,
command.screen_alert,
command.screen_confirm,
command.screen_prompt,
command.screen_select,
command.screen_local,
command.screen_host,
command.local_push,
command.local_pop,
],
})
if (props.keys.match("left", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.route.navigate(props.route.screen, { ...next, tab: (next.tab - 1 + tabs.length) % tabs.length })
return
}
if (props.keys.match("right", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.route.navigate(props.route.screen, { ...next, tab: (next.tab + 1) % tabs.length })
return
}
if (props.keys.match("up", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.route.navigate(props.route.screen, { ...next, count: next.count + 1 })
return
}
if (props.keys.match("down", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.route.navigate(props.route.screen, { ...next, count: next.count - 1 })
return
}
if (props.keys.match("modal", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.route.navigate(props.route.modal, next)
return
}
if (props.keys.match("local", evt)) {
evt.preventDefault()
evt.stopPropagation()
open()
return
}
if (props.keys.match("host", evt)) {
evt.preventDefault()
evt.stopPropagation()
host(props.api, props.input, skin)
return
}
if (props.keys.match("alert", evt)) {
evt.preventDefault()
evt.stopPropagation()
warn(props.api, props.route, next)
return
}
if (props.keys.match("confirm", evt)) {
evt.preventDefault()
evt.stopPropagation()
check(props.api, props.route, next)
return
}
if (props.keys.match("prompt", evt)) {
evt.preventDefault()
evt.stopPropagation()
entry(props.api, props.route, next)
return
}
if (props.keys.match("select", evt)) {
evt.preventDefault()
evt.stopPropagation()
picker(props.api, props.route, next)
return {
screen_home: props.api.keys.formatBindings(bindings.get(command.screen_home)) ?? "",
screen_up: props.api.keys.formatBindings(bindings.get(command.screen_up)) ?? "",
screen_down: props.api.keys.formatBindings(bindings.get(command.screen_down)) ?? "",
screen_modal: props.api.keys.formatBindings(bindings.get(command.screen_modal)) ?? "",
screen_alert: props.api.keys.formatBindings(bindings.get(command.screen_alert)) ?? "",
screen_confirm: props.api.keys.formatBindings(bindings.get(command.screen_confirm)) ?? "",
screen_prompt: props.api.keys.formatBindings(bindings.get(command.screen_prompt)) ?? "",
screen_select: props.api.keys.formatBindings(bindings.get(command.screen_select)) ?? "",
screen_local: props.api.keys.formatBindings(bindings.get(command.screen_local)) ?? "",
screen_host: props.api.keys.formatBindings(bindings.get(command.screen_host)) ?? "",
local_push: props.api.keys.formatBindings(bindings.get(command.local_push)) ?? "",
local_pop: props.api.keys.formatBindings(bindings.get(command.local_pop)) ?? "",
}
})
@@ -430,7 +509,7 @@ const Screen = (props: {
<b>{props.input.label} screen</b>
<span style={{ fg: skin.muted }}> plugin route</span>
</text>
<text fg={skin.muted}>{props.keys.print("home")} home</text>
<text fg={skin.muted}>{shortcuts().screen_home} home</text>
</box>
<box flexDirection="row" gap={1} paddingBottom={1}>
@@ -477,7 +556,7 @@ const Screen = (props: {
<box flexDirection="column" gap={1}>
<text fg={skin.text}>Counter: {value.count}</text>
<text fg={skin.muted}>
{props.keys.print("up")} / {props.keys.print("down")} change value
{shortcuts().screen_up} / {shortcuts().screen_down} change value
</text>
</box>
) : null}
@@ -485,17 +564,16 @@ const Screen = (props: {
{value.tab === 2 ? (
<box flexDirection="column" gap={1}>
<text fg={skin.muted}>
{props.keys.print("modal")} modal | {props.keys.print("alert")} alert | {props.keys.print("confirm")}{" "}
confirm | {props.keys.print("prompt")} prompt | {props.keys.print("select")} select
{shortcuts().screen_modal} modal | {shortcuts().screen_alert} alert | {shortcuts().screen_confirm}{" "}
confirm | {shortcuts().screen_prompt} prompt | {shortcuts().screen_select} select
</text>
<text fg={skin.muted}>
{props.keys.print("local")} local stack | {props.keys.print("host")} host stack
{shortcuts().screen_local} local stack | {shortcuts().screen_host} host stack
</text>
<text fg={skin.muted}>
local open: {props.keys.print("local_push")} push nested · esc or {props.keys.print("local_close")}{" "}
close
local open: {shortcuts().local_push} push nested · {shortcuts().local_pop} close
</text>
<text fg={skin.muted}>{props.keys.print("home")} returns home</text>
<text fg={skin.muted}>{shortcuts().screen_home} returns home</text>
</box>
) : null}
</box>
@@ -548,7 +626,7 @@ const Screen = (props: {
</text>
<text fg={skin.muted}>Plugin-owned stack depth: {value.local}</text>
<text fg={skin.muted}>
{props.keys.print("local_push")} push nested · {props.keys.print("local_close")} pop/close
{shortcuts().local_push} push nested · {shortcuts().local_pop} pop/close
</text>
<box flexDirection="row" gap={1}>
<Btn txt="push" run={push} skin={skin} on />
@@ -571,20 +649,35 @@ const Modal = (props: {
const value = parse(props.params)
const skin = tone(props.api)
useKeyboard((evt) => {
if (props.api.route.current.name !== props.route.modal) return
useBindings(() => ({
enabled: () => props.api.route.current.name === props.route.modal,
commands: [
{
name: command.modal_accept,
run() {
props.api.route.navigate(props.route.screen, { ...parse(props.params), source: "modal" })
},
},
{
name: command.modal_close,
run() {
props.api.route.navigate("home")
},
},
],
bindings: props.keys.gather("smoke.modal", [command.modal_accept, command.modal_close]),
}))
const shortcuts = useKeymapSelector((keymap) => {
const bindings = keymap.getCommandBindings({
visibility: "registered",
commands: [command.modal, command.screen, command.modal_accept, command.modal_close],
})
if (props.keys.match("modal_accept", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.route.navigate(props.route.screen, { ...value, source: "modal" })
return
}
if (props.keys.match("modal_close", evt)) {
evt.preventDefault()
evt.stopPropagation()
props.api.route.navigate("home")
return {
modal: props.api.keys.formatBindings(bindings.get(command.modal)) ?? "",
screen: props.api.keys.formatBindings(bindings.get(command.screen)) ?? "",
modal_accept: props.api.keys.formatBindings(bindings.get(command.modal_accept)) ?? "",
modal_close: props.api.keys.formatBindings(bindings.get(command.modal_close)) ?? "",
}
})
@@ -595,10 +688,10 @@ const Modal = (props: {
<text fg={skin.text}>
<b>{props.input.label} modal</b>
</text>
<text fg={skin.muted}>{props.keys.print("modal")} modal command</text>
<text fg={skin.muted}>{props.keys.print("screen")} screen command</text>
<text fg={skin.muted}>{shortcuts().modal} modal command</text>
<text fg={skin.muted}>{shortcuts().screen} screen command</text>
<text fg={skin.muted}>
{props.keys.print("modal_accept")} opens screen · {props.keys.print("modal_close")} closes
{shortcuts().modal_accept} opens screen · {shortcuts().modal_close} closes
</text>
<box flexDirection="row" gap={1}>
<Btn
@@ -651,25 +744,8 @@ const home = (api: TuiPluginApi, input: Cfg) => ({
},
home_prompt(ctx, value) {
const skin = look(ctx.theme.current)
type Prompt = (props: {
workspaceID?: string
visible?: boolean
disabled?: boolean
onSubmit?: () => void
hint?: JSX.Element
right?: JSX.Element
showPlaceholder?: boolean
placeholders?: {
normal?: string[]
shell?: string[]
}
}) => JSX.Element
type Slot = (
props: { name: string; mode?: unknown; children?: JSX.Element } & Record<string, unknown>,
) => JSX.Element | null
const ui = api.ui as TuiPluginApi["ui"] & { Prompt: Prompt; Slot: Slot }
const Prompt = ui.Prompt
const Slot = ui.Slot
const Prompt = api.ui.Prompt
const Slot = api.ui.Slot
const normal = [
`[SMOKE] route check for ${input.label}`,
"[SMOKE] confirm home_prompt slot override",
@@ -791,109 +867,115 @@ const slot = (api: TuiPluginApi, input: Cfg): TuiSlotPlugin[] => [
const reg = (api: TuiPluginApi, input: Cfg, keys: Keys) => {
const route = names(input)
api.command.register(() => [
{
title: `${input.label} modal`,
value: "plugin.smoke.modal",
keybind: keys.get("modal"),
category: "Plugin",
slash: {
name: "smoke",
api.keymap.registerLayer({
commands: [
{
name: command.modal,
title: `${input.label} modal`,
category: "Plugin",
namespace: "palette",
slashName: "smoke",
run() {
api.route.navigate(route.modal, { source: "command" })
},
},
onSelect: () => {
api.route.navigate(route.modal, { source: "command" })
{
name: command.screen,
title: `${input.label} screen`,
category: "Plugin",
namespace: "palette",
slashName: "smoke-screen",
run() {
api.route.navigate(route.screen, { source: "command", tab: 0, count: 0 })
},
},
},
{
title: `${input.label} screen`,
value: "plugin.smoke.screen",
keybind: keys.get("screen"),
category: "Plugin",
slash: {
name: "smoke-screen",
{
name: command.alert,
title: `${input.label} alert dialog`,
category: "Plugin",
namespace: "palette",
slashName: "smoke-alert",
run() {
warn(api, route, current(api, route))
},
},
onSelect: () => {
api.route.navigate(route.screen, { source: "command", tab: 0, count: 0 })
{
name: command.confirm,
title: `${input.label} confirm dialog`,
category: "Plugin",
namespace: "palette",
slashName: "smoke-confirm",
run() {
check(api, route, current(api, route))
},
},
},
{
title: `${input.label} alert dialog`,
value: "plugin.smoke.alert",
category: "Plugin",
slash: {
name: "smoke-alert",
{
name: command.prompt,
title: `${input.label} prompt dialog`,
category: "Plugin",
namespace: "palette",
slashName: "smoke-prompt",
run() {
entry(api, route, current(api, route))
},
},
onSelect: () => {
warn(api, route, current(api, route))
{
name: command.select,
title: `${input.label} select dialog`,
category: "Plugin",
namespace: "palette",
slashName: "smoke-select",
run() {
picker(api, route, current(api, route))
},
},
},
{
title: `${input.label} confirm dialog`,
value: "plugin.smoke.confirm",
category: "Plugin",
slash: {
name: "smoke-confirm",
{
name: command.host,
title: `${input.label} host overlay`,
category: "Plugin",
namespace: "palette",
slashName: "smoke-host",
run() {
host(api, input, tone(api))
},
},
onSelect: () => {
check(api, route, current(api, route))
{
name: command.home,
title: `${input.label} go home`,
category: "Plugin",
namespace: "palette",
enabled: () => api.route.current.name !== "home",
run() {
api.route.navigate("home")
},
},
},
{
title: `${input.label} prompt dialog`,
value: "plugin.smoke.prompt",
category: "Plugin",
slash: {
name: "smoke-prompt",
{
name: command.toast,
title: `${input.label} toast`,
category: "Plugin",
namespace: "palette",
run() {
api.ui.toast({
variant: "info",
title: "Smoke",
message: "Plugin toast works",
duration: 2000,
})
},
},
onSelect: () => {
entry(api, route, current(api, route))
},
},
{
title: `${input.label} select dialog`,
value: "plugin.smoke.select",
category: "Plugin",
slash: {
name: "smoke-select",
},
onSelect: () => {
picker(api, route, current(api, route))
},
},
{
title: `${input.label} host overlay`,
value: "plugin.smoke.host",
category: "Plugin",
slash: {
name: "smoke-host",
},
onSelect: () => {
host(api, input, tone(api))
},
},
{
title: `${input.label} go home`,
value: "plugin.smoke.home",
category: "Plugin",
enabled: api.route.current.name !== "home",
onSelect: () => {
api.route.navigate("home")
},
},
{
title: `${input.label} toast`,
value: "plugin.smoke.toast",
category: "Plugin",
onSelect: () => {
api.ui.toast({
variant: "info",
title: "Smoke",
message: "Plugin toast works",
duration: 2000,
})
},
},
])
],
bindings: keys.gather("smoke.global", [
command.modal,
command.screen,
command.alert,
command.confirm,
command.prompt,
command.select,
command.host,
command.home,
command.toast,
]),
})
}
const tui: TuiPlugin = async (api, options, meta) => {
@@ -902,9 +984,9 @@ const tui: TuiPlugin = async (api, options, meta) => {
await api.theme.install("./smoke-theme.json")
api.theme.set("smoke-theme")
const value = cfg(options ?? undefined)
const value = cfg(options)
const route = names(value)
const keys = api.keybind.create(bind, value.keybinds)
const keys = createKeys(value.keybinds)
const fx = new VignetteEffect(value.vignette)
const post = fx.apply.bind(fx)
api.renderer.addPostProcessFn(post)

View File

@@ -0,0 +1,37 @@
# Deepening
How to deepen a cluster of shallow modules safely, given its dependencies. Assumes the vocabulary in [LANGUAGE.md](LANGUAGE.md) — **module**, **interface**, **seam**, **adapter**.
## Dependency categories
When assessing a candidate for deepening, classify its dependencies. The category determines how the deepened module is tested across its seam.
### 1. In-process
Pure computation, in-memory state, no I/O. Always deepenable — merge the modules and test through the new interface directly. No adapter needed.
### 2. Local-substitutable
Dependencies that have local test stand-ins (PGLite for Postgres, in-memory filesystem). Deepenable if the stand-in exists. The deepened module is tested with the stand-in running in the test suite. The seam is internal; no port at the module's external interface.
### 3. Remote but owned (Ports & Adapters)
Your own services across a network boundary (microservices, internal APIs). Define a **port** (interface) at the seam. The deep module owns the logic; the transport is injected as an **adapter**. Tests use an in-memory adapter. Production uses an HTTP/gRPC/queue adapter.
Recommendation shape: _"Define a port at the seam, implement an HTTP adapter for production and an in-memory adapter for testing, so the logic sits in one deep module even though it's deployed across a network."_
### 4. True external (Mock)
Third-party services (Stripe, Twilio, etc.) you don't control. The deepened module takes the external dependency as an injected port; tests provide a mock adapter.
## Seam discipline
- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a port unless at least two adapters are justified (typically production + test). A single-adapter seam is just indirection.
- **Internal seams vs external seams.** A deep module can have internal seams (private to its implementation, used by its own tests) as well as the external seam at its interface. Don't expose internal seams through the interface just because tests use them.
## Testing strategy: replace, don't layer
- Old unit tests on shallow modules become waste once tests at the deepened module's interface exist — delete them.
- Write new tests at the deepened module's interface. The **interface is the test surface**.
- Tests assert on observable outcomes through the interface, not internal state.
- Tests should survive internal refactors — they describe behaviour, not implementation. If a test has to change when the implementation changes, it's testing past the interface.

View File

@@ -0,0 +1,44 @@
# Interface Design
When the user wants to explore alternative interfaces for a chosen deepening candidate, use this parallel sub-agent pattern. Based on "Design It Twice" (Ousterhout) — your first idea is unlikely to be the best.
Uses the vocabulary in [LANGUAGE.md](LANGUAGE.md) — **module**, **interface**, **seam**, **adapter**, **leverage**.
## Process
### 1. Frame the problem space
Before spawning sub-agents, write a user-facing explanation of the problem space for the chosen candidate:
- The constraints any new interface would need to satisfy
- The dependencies it would rely on, and which category they fall into (see [DEEPENING.md](DEEPENING.md))
- A rough illustrative code sketch to ground the constraints — not a proposal, just a way to make the constraints concrete
Show this to the user, then immediately proceed to Step 2. The user reads and thinks while the sub-agents work in parallel.
### 2. Spawn sub-agents
Spawn 3+ sub-agents in parallel using the Agent tool. Each must produce a **radically different** interface for the deepened module.
Prompt each sub-agent with a separate technical brief (file paths, coupling details, dependency category from [DEEPENING.md](DEEPENING.md), what sits behind the seam). The brief is independent of the user-facing problem-space explanation in Step 1. Give each agent a different design constraint:
- Agent 1: "Minimize the interface — aim for 13 entry points max. Maximise leverage per entry point."
- Agent 2: "Maximise flexibility — support many use cases and extension."
- Agent 3: "Optimise for the most common caller — make the default case trivial."
- Agent 4 (if applicable): "Design around ports & adapters for cross-seam dependencies."
Include both [LANGUAGE.md](LANGUAGE.md) vocabulary and CONTEXT.md vocabulary in the brief so each sub-agent names things consistently with the architecture language and the project's domain language.
Each sub-agent outputs:
1. Interface (types, methods, params — plus invariants, ordering, error modes)
2. Usage example showing how callers use it
3. What the implementation hides behind the seam
4. Dependency strategy and adapters (see [DEEPENING.md](DEEPENING.md))
5. Trade-offs — where leverage is high, where it's thin
### 3. Present and compare
Present designs sequentially so the user can absorb each one, then compare them in prose. Contrast by **depth** (leverage at the interface), **locality** (where change concentrates), and **seam placement**.
After comparing, give your own recommendation: which design you think is strongest and why. If elements from different designs would combine well, propose a hybrid. Be opinionated — the user wants a strong read, not a menu.

View File

@@ -0,0 +1,53 @@
# Language
Shared vocabulary for every suggestion this skill makes. Use these terms exactly — don't substitute "component," "service," "API," or "boundary." Consistent language is the whole point.
## Terms
**Module**
Anything with an interface and an implementation. Deliberately scale-agnostic — applies equally to a function, class, package, or tier-spanning slice.
_Avoid_: unit, component, service.
**Interface**
Everything a caller must know to use the module correctly. Includes the type signature, but also invariants, ordering constraints, error modes, required configuration, and performance characteristics.
_Avoid_: API, signature (too narrow — those refer only to the type-level surface).
**Implementation**
What's inside a module — its body of code. Distinct from **Adapter**: a thing can be a small adapter with a large implementation (a Postgres repo) or a large adapter with a small implementation (an in-memory fake). Reach for "adapter" when the seam is the topic; "implementation" otherwise.
**Depth**
Leverage at the interface — the amount of behaviour a caller (or test) can exercise per unit of interface they have to learn. A module is **deep** when a large amount of behaviour sits behind a small interface. A module is **shallow** when the interface is nearly as complex as the implementation.
**Seam** _(from Michael Feathers)_
A place where you can alter behaviour without editing in that place. The _location_ at which a module's interface lives. Choosing where to put the seam is its own design decision, distinct from what goes behind it.
_Avoid_: boundary (overloaded with DDD's bounded context).
**Adapter**
A concrete thing that satisfies an interface at a seam. Describes _role_ (what slot it fills), not substance (what's inside).
**Leverage**
What callers get from depth. More capability per unit of interface they have to learn. One implementation pays back across N call sites and M tests.
**Locality**
What maintainers get from depth. Change, bugs, knowledge, and verification concentrate at one place rather than spreading across callers. Fix once, fixed everywhere.
## Principles
- **Depth is a property of the interface, not the implementation.** A deep module can be internally composed of small, mockable, swappable parts — they just aren't part of the interface. A module can have **internal seams** (private to its implementation, used by its own tests) as well as the **external seam** at its interface.
- **The deletion test.** Imagine deleting the module. If complexity vanishes, the module wasn't hiding anything (it was a pass-through). If complexity reappears across N callers, the module was earning its keep.
- **The interface is the test surface.** Callers and tests cross the same seam. If you want to test _past_ the interface, the module is probably the wrong shape.
- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a seam unless something actually varies across it.
## Relationships
- A **Module** has exactly one **Interface** (the surface it presents to callers and tests).
- **Depth** is a property of a **Module**, measured against its **Interface**.
- A **Seam** is where a **Module**'s **Interface** lives.
- An **Adapter** sits at a **Seam** and satisfies the **Interface**.
- **Depth** produces **Leverage** for callers and **Locality** for maintainers.
## Rejected framings
- **Depth as ratio of implementation-lines to interface-lines** (Ousterhout): rewards padding the implementation. We use depth-as-leverage instead.
- **"Interface" as the TypeScript `interface` keyword or a class's public methods**: too narrow — interface here includes every fact a caller must know.
- **"Boundary"**: overloaded with DDD's bounded context. Say **seam** or **interface**.

View File

@@ -0,0 +1,71 @@
---
name: improve-codebase-architecture
description: Find deepening opportunities in a codebase, informed by the domain language in CONTEXT.md and the decisions in docs/adr/. Use when the user wants to improve architecture, find refactoring opportunities, consolidate tightly-coupled modules, or make a codebase more testable and AI-navigable.
---
# Improve Codebase Architecture
Surface architectural friction and propose **deepening opportunities** — refactors that turn shallow modules into deep ones. The aim is testability and AI-navigability.
## Glossary
Use these terms exactly in every suggestion. Consistent language is the point — don't drift into "component," "service," "API," or "boundary." Full definitions in [LANGUAGE.md](LANGUAGE.md).
- **Module** — anything with an interface and an implementation (function, class, package, slice).
- **Interface** — everything a caller must know to use the module: types, invariants, error modes, ordering, config. Not just the type signature.
- **Implementation** — the code inside.
- **Depth** — leverage at the interface: a lot of behaviour behind a small interface. **Deep** = high leverage. **Shallow** = interface nearly as complex as the implementation.
- **Seam** — where an interface lives; a place behaviour can be altered without editing in place. (Use this, not "boundary.")
- **Adapter** — a concrete thing satisfying an interface at a seam.
- **Leverage** — what callers get from depth.
- **Locality** — what maintainers get from depth: change, bugs, knowledge concentrated in one place.
Key principles (see [LANGUAGE.md](LANGUAGE.md) for the full list):
- **Deletion test**: imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep.
- **The interface is the test surface.**
- **One adapter = hypothetical seam. Two adapters = real seam.**
This skill is _informed_ by the project's domain model. The domain language gives names to good seams; ADRs record decisions the skill should not re-litigate.
## Process
### 1. Explore
Read the project's domain glossary and any ADRs in the area you're touching first.
Then use the Agent tool with `subagent_type=Explore` to walk the codebase. Don't follow rigid heuristics — explore organically and note where you experience friction:
- Where does understanding one concept require bouncing between many small modules?
- Where are modules **shallow** — interface nearly as complex as the implementation?
- Where have pure functions been extracted just for testability, but the real bugs hide in how they're called (no **locality**)?
- Where do tightly-coupled modules leak across their seams?
- Which parts of the codebase are untested, or hard to test through their current interface?
Apply the **deletion test** to anything you suspect is shallow: would deleting it concentrate complexity, or just move it? A "yes, concentrates" is the signal you want.
### 2. Present candidates
Present a numbered list of deepening opportunities. For each candidate:
- **Files** — which files/modules are involved
- **Problem** — why the current architecture is causing friction
- **Solution** — plain English description of what would change
- **Benefits** — explained in terms of locality and leverage, and also in how tests would improve
**Use CONTEXT.md vocabulary for the domain, and [LANGUAGE.md](LANGUAGE.md) vocabulary for the architecture.** If `CONTEXT.md` defines "Order," talk about "the Order intake module" — not "the FooBarHandler," and not "the Order service."
**ADR conflicts**: if a candidate contradicts an existing ADR, only surface it when the friction is real enough to warrant revisiting the ADR. Mark it clearly (e.g. _"contradicts ADR-0007 — but worth reopening because…"_). Don't list every theoretical refactor an ADR forbids.
Do NOT propose interfaces yet. Ask the user: "Which of these would you like to explore?"
### 3. Grilling loop
Once the user picks a candidate, drop into a grilling conversation. Walk the design tree with them — constraints, dependencies, the shape of the deepened module, what sits behind the seam, what tests survive.
Side effects happen inline as decisions crystallize:
- **Naming a deepened module after a concept not in `CONTEXT.md`?** Add the term to `CONTEXT.md` — same discipline as `/grill-with-docs` (see [CONTEXT-FORMAT.md](../grill-with-docs/CONTEXT-FORMAT.md)). Create the file lazily if it doesn't exist.
- **Sharpening a fuzzy term during the conversation?** Update `CONTEXT.md` right there.
- **User rejects the candidate with a load-bearing reason?** Offer an ADR, framed as: _"Want me to record this as an ADR so future architecture reviews don't re-suggest it?"_ Only offer when the reason would actually be needed by a future explorer to avoid re-suggesting the same thing — skip ephemeral reasons ("not worth it right now") and self-evident ones. See [ADR-FORMAT.md](../grill-with-docs/ADR-FORMAT.md).
- **Want to explore alternative interfaces for the deepened module?** See [INTERFACE-DESIGN.md](INTERFACE-DESIGN.md).

View File

@@ -7,10 +7,11 @@
"enabled": false,
"label": "workspace",
"keybinds": {
"modal": "ctrl+alt+m",
"screen": "ctrl+alt+o",
"home": "escape,ctrl+shift+h",
"dialog_close": "escape,q"
"smoke_modal": "ctrl+alt+m",
"smoke_screen": "ctrl+alt+o",
"smoke_screen_home": "escape,ctrl+shift+h",
"smoke_screen_modal": "ctrl+alt+m",
"smoke_dialog_close": "escape,q"
}
}
]

View File

@@ -9,6 +9,7 @@
### General Principles
- Keep things in one function unless composable or reusable
- Do not extract single-use helpers preemptively. Inline the logic at the call site unless the helper is reused, hides a genuinely complex boundary, or has a clear independent name that improves the caller.
- Avoid `try`/`catch` where possible
- Avoid using the `any` type
- Use Bun APIs when possible, like `Bun.file()`
@@ -72,6 +73,29 @@ function foo() {
}
```
### Complex Logic
When a function has several validation branches or supporting details, make the main function read as the happy path and move supporting details into small helpers below it.
```ts
// Good
export function loadThing(input: unknown) {
const config = requireConfig(input)
const metadata = readMetadata(input)
return createThing({ config, metadata })
}
function requireConfig(input: unknown) {
...
}
```
- Keep helpers close to the code they support, below the main export when that improves readability.
- Do not over-abstract simple expressions into many single-use helpers; extract only when it names a real concept like `requireConfig` or `readMetadata`.
- Do not return `Effect` from helpers unless they actually perform effectful work. Synchronous parsing, validation, and option building should stay synchronous.
- Prefer Effect schema helpers such as `Schema.UnknownFromJsonString` and `Schema.decodeUnknownOption` over manual `JSON.parse` wrapped in `Effect.try` when parsing untrusted JSON strings.
- Add comments for non-obvious constraints and surprising behavior, not for obvious assignments or control flow.
### Schema Definitions (Drizzle)
Use snake_case for field names so column names don't need to be redefined as strings.

554
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,8 @@
[install]
exact = true
# Only install newly resolved package versions published at least 3 days ago.
minimumReleaseAge = 259200
minimumReleaseAgeExcludes = ["@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-x64", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid"]
[test]
root = "./do-not-run-tests-from-root"

View File

@@ -30,6 +30,7 @@ export const api = new sst.cloudflare.Worker("Api", {
transform: {
worker: (args) => {
args.logpush = true
if ($app.stage === "vimtor") return
args.bindings = $resolve(args.bindings).apply((bindings) => [
...bindings,
{

View File

@@ -1,5 +1,6 @@
import { domain } from "./stage"
import { EMAILOCTOPUS_API_KEY } from "./app"
import { SECRET } from "./secret"
////////////////
// DATABASE
@@ -221,8 +222,6 @@ const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
const STRIPE_WEBHOOK_SECRET = new sst.Linkable("STRIPE_WEBHOOK_SECRET", {
properties: { value: stripeWebhook.secret },
})
const INCIDENT_WEBHOOK_SIGNING_SECRET = new sst.Secret("INCIDENT_WEBHOOK_SIGNING_SECRET")
const DISCORD_INCIDENT_WEBHOOK_URL = new sst.Secret("DISCORD_INCIDENT_WEBHOOK_URL")
const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
@@ -233,6 +232,7 @@ const gatewayKv = new sst.cloudflare.Kv("GatewayKv")
const bucket = new sst.cloudflare.Bucket("ZenData")
const bucketNew = new sst.cloudflare.Bucket("ZenDataNew")
const DISCORD_INCIDENT_WEBHOOK_URL = new sst.Secret("DISCORD_INCIDENT_WEBHOOK_URL")
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
@@ -254,8 +254,8 @@ new sst.cloudflare.x.SolidStart("Console", {
database,
AUTH_API_URL,
STRIPE_WEBHOOK_SECRET,
INCIDENT_WEBHOOK_SIGNING_SECRET,
DISCORD_INCIDENT_WEBHOOK_URL,
SECRET.HoneycombWebhookSecret,
STRIPE_SECRET_KEY,
EMAILOCTOPUS_API_KEY,
AWS_SES_ACCESS_KEY_ID,
@@ -293,3 +293,13 @@ new sst.cloudflare.x.SolidStart("Console", {
},
},
})
////////////////
// HELPERS
////////////////
export const stat = new sst.cloudflare.Worker("Stat", {
handler: "packages/console/function/src/stat.ts",
link: [database],
url: true,
})

View File

@@ -1,318 +1,282 @@
const displayName = (s: string) =>
s
.split("-")
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join(" ")
.replace(/(?<=\d) (?=\d)/g, ".")
import { SECRET } from "./secret"
import { domain } from "./stage"
const resourceName = (s: string) => displayName(s).replace(/[^a-zA-Z0-9]/g, "")
const description = "Managed by SST (Don't edit in Honeycomb UI)"
const varSpec = (label: string, name: string) =>
$jsonStringify({
content: [
{
content: [
{
attrs: {
name,
label,
missing: false,
},
type: "varSpec",
},
],
type: "paragraph",
},
],
type: "doc",
})
const fields = {
model: incident.getAlertAttributeOutput({ name: "Model" }),
product: incident.getAlertAttributeOutput({ name: "Product" }),
}
const alertSource = new incident.AlertSource("HoneycombAlertSource", {
name: $app.stage === "production" ? "Honeycomb" : `Honeycomb (${$app.stage})`,
sourceType: "honeycomb",
template: {
title: {
literal: varSpec("Payload -> Title", "title"),
},
description: {
literal: varSpec("Payload -> Description", "description"),
},
attributes: [
{
alertAttributeId: fields.model.id,
binding: {
value: {
reference: 'expressions["model"]',
},
mergeStrategy: "first_wins",
},
},
{
alertAttributeId: fields.product.id,
binding: {
value: {
reference: 'expressions["product"]',
},
mergeStrategy: "first_wins",
},
},
],
expressions: [
{
label: "Model",
operations: [
{
operationType: "parse",
parse: {
returns: {
array: false,
type: fields.model.type,
},
source: "$['model']",
},
},
],
reference: "model",
rootReference: "payload",
},
{
label: "Product",
operations: [
{
operationType: "parse",
parse: {
returns: {
array: false,
type: fields.product.type,
},
source: "$['product']",
},
},
],
reference: "product",
rootReference: "payload",
},
],
},
})
const webhookRecipient = new honeycomb.WebhookRecipient(`IncidentWebhook`, {
name: $app.stage === "production" ? "Incident.io" : `Incident.io (${$app.stage})`,
url: alertSource.alertEventsUrl,
secret: alertSource.secretToken,
const webhookRecipient = new honeycomb.WebhookRecipient("DiscordAlerts", {
name: $app.stage === "production" ? "Discord Alerts" : `Discord Alerts (${$app.stage})`,
url: `https://${domain}/honeycomb/webhook`,
secret: SECRET.HoneycombWebhookSecret.result,
templates: [
{
type: "trigger",
body: $jsonStringify({
title: "{{ .Name }}",
description: "{{ .Description }}",
status: "{{ .Alert.Status }}",
deduplication_key: "{{ .Alert.InstanceID }}",
source_url: "{{ .Result.URL }}",
model: "{{ .Vars.model }}",
product: "{{ .Vars.product }}",
}),
body: `{
"url": {{ .Result.URL | quote }},
"type": {{ .Vars.type | quote }},
"name": {{ .Name | quote }},
"status": {{ .Alert.Status | quote }},
"isTest": {{ .Alert.IsTest }},
"groups": {{ .Result.GroupsTriggered | toJson }}
}`,
},
],
variables: [
{
name: "model",
},
{
name: "product",
name: "type",
},
],
})
new incident.AlertRoute("HoneycombAlertRoute", {
name: $app.stage === "production" ? "Honeycomb" : `Honeycomb (${$app.stage})`,
enabled: true,
isPrivate: false,
alertSources: [
{
alertSourceId: alertSource.id,
conditionGroups: [
{
conditions: [
{
subject: "alert.title",
operation: "is_set",
paramBindings: [],
},
],
},
],
},
],
conditionGroups: [
{
conditions: [
{
subject: "alert.title",
operation: "is_set",
paramBindings: [],
},
],
},
],
expressions: [],
escalationConfig: {
autoCancelEscalations: true,
escalationTargets: [],
},
incidentConfig: {
autoDeclineEnabled: true,
enabled: true,
conditionGroups: [],
deferTimeSeconds: 0,
groupingKeys: [
// Honeycomb can keep stale query-local calculated fields when the name is unchanged,
// so tie the field name to the expression while avoiding deploy-to-deploy churn.
// https://github.com/honeycombio/terraform-provider-honeycombio/issues/852
const calculatedField = (field: { name: string; expression: string }) => ({
...field,
name: `${field.name}_${(
Array.from(field.expression).reduce((result, char) => Math.imul(31, result) + char.charCodeAt(0), 0) >>> 0
).toString(36)}`,
})
const modelHttpErrorsQuery = (product: "go" | "zen") => {
const filters = [
{ column: "model", op: "exists" },
{ column: "event_type", op: "=", value: "completions" },
{ column: "user_agent", op: "contains", value: "opencode" },
{ column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" },
]
const failedHttpStatus = calculatedField({
name: "is_failed_http_status",
expression:
product === "go"
? `IF(AND(GTE($status, "400"), NOT(EQUALS($status, "401")), NOT(EQUALS($status, "429"))), 1, 0)`
: `IF(AND(EQUALS($status, "429"), $isFreeTier), 0, AND(GTE($status, "400"), NOT(EQUALS($status, "401"))), 1, 0)`,
})
return honeycomb.getQuerySpecificationOutput({
breakdowns: ["model"],
calculatedFields: [failedHttpStatus],
calculations: [
{ op: "COUNT", name: "TOTAL", filterCombination: "AND", filters },
{
reference: $interpolate`alert.attributes.${fields.model.id}`,
},
{
reference: $interpolate`alert.attributes.${fields.product.id}`,
op: "SUM",
name: "FAILED",
column: failedHttpStatus.name,
filterCombination: "AND",
filters,
},
],
groupingWindowSeconds: 3600,
},
incidentTemplate: {
name: {
value: {
literal: varSpec("Alert -> Title", "alert.title"),
},
},
summary: {
value: {
literal: varSpec("Alert -> Description", "alert.description"),
},
},
startInTriage: {
value: {
literal: "true",
},
},
severity: {
mergeStrategy: "first-wins",
},
incidentMode: {
value: {
literal: $app.stage === "production" ? "standard" : "test",
},
},
},
})
type Product = "go" | "zen"
type Trigger = (opts: { model: string; product: Product }) => {
id: string
title: string
description: string
json: honeycomb.GetQuerySpecificationOutputArgs
threshold: { op: ">=" | "<="; value: number }
formulas: [{ name: "ERROR", expression: "IF(GTE($TOTAL, 100), DIV($FAILED, $TOTAL), 0)" }],
timeRange: 900,
}).json
}
type Model = { id: string; products: Product[]; triggers: Trigger[] }
const providerHttpErrorsQuery = (product: "go" | "zen") => {
const filters = [
{ column: "provider", op: "exists" },
{ column: "user_agent", op: "contains", value: "opencode" },
{ column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" },
]
const successHttpStatus = calculatedField({
name: "is_success_http_status",
expression: `IF(AND(GTE($status, "200"), LT($status, "400")), 1, 0)`,
})
const failedProviderHttpStatus = calculatedField({
name: "is_failed_provider_http_status",
expression: `IF(GT($llm.error.code, "400"), 1, 0)`,
})
const httpErrors: Trigger = ({ model, product }) => ({
id: "increased-http-errors",
title: `Increased HTTP Errors for ${displayName(model)} on ${displayName(product)}`,
description: `Detected increased rate of HTTP errors for ${displayName(model)} on OpenCode ${displayName(product)}`,
json: {
return honeycomb.getQuerySpecificationOutput({
breakdowns: ["provider"],
calculatedFields: [successHttpStatus, failedProviderHttpStatus],
calculations: [
{
op: "COUNT",
name: "TOTAL",
op: "SUM",
name: "SUCCESS",
column: successHttpStatus.name,
filterCombination: "AND",
filters: [
{ column: "model", op: "=", value: model },
{ column: "event_type", op: "=", value: "completions" },
{ column: "user_agent", op: "contains", value: "opencode" },
{ column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" },
],
filters: [...filters, { column: "event_type", op: "=", value: "completions" }],
},
{
op: "COUNT",
op: "SUM",
name: "FAILED",
column: failedProviderHttpStatus.name,
filterCombination: "AND",
filters: [
{ column: "model", op: "=", value: model },
{ column: "event_type", op: "=", value: "completions" },
{ column: "user_agent", op: "contains", value: "opencode" },
{ column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" },
{ column: "status", op: ">=", value: "400" },
{ column: "status", op: "!=", value: "401" },
],
filters: [...filters, { column: "event_type", op: "=", value: "llm.error" }],
},
],
formulas: [{ name: "ERROR", expression: "$FAILED / $TOTAL" }],
formulas: [
{ name: "ERROR", expression: "IF(GTE(SUM($SUCCESS, $FAILED), 50), DIV($FAILED, SUM($SUCCESS, $FAILED)), 0)" },
],
timeRange: 900,
},
threshold: { op: ">=", value: 0.8 },
}).json
}
const modelLowTpsQuery = (product: "go" | "zen") => {
const filters = [
{ column: "model", op: "exists" },
{ column: "event_type", op: "=", value: "completions" },
{ column: "user_agent", op: "contains", value: "opencode" },
{ column: "isGoTier", op: "=", value: product === "go" ? "true" : "false" },
{ column: "status", op: ">=", value: "200" },
{ column: "status", op: "<", value: "400" },
{ column: "tps.output", op: "exists" },
]
return honeycomb.getQuerySpecificationOutput({
breakdowns: ["model"],
calculations: [
{ op: "COUNT", name: "TOTAL", filterCombination: "AND", filters },
{
op: "P50",
name: "TPS",
column: "tps.output",
filterCombination: "AND",
filters,
},
],
formulas: [{ name: "LOW_TPS", expression: "IF(GTE($TOTAL, 100), $TPS, 999)" }],
timeRange: 1800,
}).json
}
new honeycomb.Trigger("IncreasedModelHttpErrorsGo", {
name: "Increased Model HTTP Errors [Go]",
description,
queryJson: modelHttpErrorsQuery("go"),
alertType: "on_change",
frequency: 300,
thresholds: [{ op: ">=", value: 0.7, exceededLimit: 1 }],
recipients: [
{
id: webhookRecipient.id,
notificationDetails: [
{
variables: [{ name: "type", value: "model_http_errors" }],
},
],
},
],
})
const models: Model[] = [
{ id: "kimi-k2.6", products: ["go", "zen"], triggers: [httpErrors] },
{ id: "kimi-k2.5", products: ["go", "zen"], triggers: [httpErrors] },
{ id: "deepseek-v4-flash", products: ["go", "zen"], triggers: [httpErrors] },
{ id: "deepseek-v4-pro", products: ["go", "zen"], triggers: [httpErrors] },
{ id: "glm-5.1", products: ["go", "zen"], triggers: [httpErrors] },
// { id: "glm-5", products: ["go"], triggers: [httpErrors] },
{ id: "qwen3.6-plus", products: ["go", "zen"], triggers: [httpErrors] },
{ id: "qwen3.5-plus", products: ["go"], triggers: [httpErrors] },
{ id: "minimax-m2.7", products: ["go", "zen"], triggers: [httpErrors] },
// { id: "minimax-m2.5", products: ["go", "zen"], triggers: [httpErrors] },
{ id: "mimo-v2.5-pro", products: ["go"], triggers: [httpErrors] },
// { id: "mimo-v2.5", products: ["go"], triggers: [httpErrors] },
// { id: "mimo-v2-omni", products: ["go"], triggers: [httpErrors] },
// { id: "mimo-v2-pro", products: ["go"], triggers: [httpErrors] },
{ id: "claude-opus-4-7", products: ["zen"], triggers: [httpErrors] },
// { id: "claude-opus-4-6", products: ["zen"], triggers: [httpErrors] },
// { id: "claude-sonnet-4-6", products: ["zen"], triggers: [httpErrors] },
{ id: "gpt-5.5", products: ["zen"], triggers: [httpErrors] },
{ id: "big-pickle", products: ["zen"], triggers: [httpErrors] },
// { id: "minimax-m2.5-free", products: ["zen"], triggers: [httpErrors] },
// { id: "hy3-preview-free", products: ["zen"], triggers: [httpErrors] },
// { id: "nemotron-3-super-free", products: ["zen"], triggers: [httpErrors] },
// { id: "trinity-large-preview-free", products: ["zen"], triggers: [httpErrors] },
// { id: "ling-2.6-flash-free", products: ["zen"], triggers: [httpErrors] },
]
new honeycomb.Trigger("IncreasedModelHttpErrorsZen", {
name: "Increased Model HTTP Errors [Zen]",
description,
queryJson: modelHttpErrorsQuery("zen"),
alertType: "on_change",
frequency: 300,
thresholds: [{ op: ">=", value: 0.7, exceededLimit: 1 }],
recipients: [
{
id: webhookRecipient.id,
notificationDetails: [
{
variables: [{ name: "type", value: "model_http_errors" }],
},
],
},
],
})
if ($app.stage !== "production") {
models.splice(1)
}
new honeycomb.Trigger("LowModelTpsGo", {
name: "Low Model TPS [Go]",
description,
queryJson: modelLowTpsQuery("go"),
alertType: "on_change",
frequency: 600,
thresholds: [{ op: "<=", value: 10, exceededLimit: 1 }],
recipients: [
{
id: webhookRecipient.id,
notificationDetails: [
{
variables: [{ name: "type", value: "model_low_tps" }],
},
],
},
],
})
for (const model of models) {
for (const product of model.products) {
for (const trigger of model.triggers) {
const spec = trigger({ model: model.id, product })
new honeycomb.Trigger("LowModelTpsZen", {
name: "Low Model TPS [Zen]",
description,
queryJson: modelLowTpsQuery("zen"),
alertType: "on_change",
frequency: 600,
thresholds: [{ op: "<=", value: 10, exceededLimit: 1 }],
recipients: [
{
id: webhookRecipient.id,
notificationDetails: [
{
variables: [{ name: "type", value: "model_low_tps" }],
},
],
},
],
})
new honeycomb.Trigger(resourceName(`${spec.id}-${product}-${model.id}`), {
name: spec.title,
description: spec.description,
queryJson: honeycomb.getQuerySpecificationOutput(spec.json).json,
alertType: "on_change",
frequency: 300,
thresholds: [{ ...spec.threshold, exceededLimit: 1 }],
recipients: [
{
id: webhookRecipient.id,
notificationDetails: [
{
variables: [
{ name: "model", value: model.id },
{ name: "product", value: product },
],
},
],
},
],
})
}
}
}
new honeycomb.Trigger("IncreasedProviderHttpErrorsGo", {
name: "Increased Provider HTTP Errors [Go]",
description,
queryJson: providerHttpErrorsQuery("go"),
alertType: "on_change",
frequency: 300,
thresholds: [{ op: ">=", value: 0.7, exceededLimit: 1 }],
recipients: [
{
id: webhookRecipient.id,
notificationDetails: [
{
variables: [{ name: "type", value: "provider_http_errors" }],
},
],
},
],
})
new honeycomb.Trigger("IncreasedProviderHttpErrorsZen", {
name: "Increased Provider HTTP Errors [Zen]",
description,
queryJson: providerHttpErrorsQuery("zen"),
alertType: "on_change",
frequency: 300,
thresholds: [{ op: ">=", value: 0.7, exceededLimit: 1 }],
recipients: [
{
id: webhookRecipient.id,
notificationDetails: [
{
variables: [{ name: "type", value: "provider_http_errors" }],
},
],
},
],
})
new honeycomb.Trigger("IncreasedFreeTierRequests", {
name: "Increased Free Tier Requests",
description,
queryJson: honeycomb.getQuerySpecificationOutput({
calculations: [{ op: "COUNT" }],
filters: [
{ column: "event_type", op: "=", value: "completions" },
{ column: "user_agent", op: "contains", value: "opencode" },
{ column: "isFreeTier", op: "=", value: "true" },
],
timeRange: 3600,
}).json,
alertType: "on_change",
frequency: 900,
thresholds: [{ op: ">=", value: 50, exceededLimit: 1 }],
baselineDetails: [{ type: "percentage", offsetMinutes: 1440 }],
recipients: [
{
id: webhookRecipient.id,
notificationDetails: [
{
variables: [{ name: "type", value: "custom" }],
},
],
},
],
})

View File

@@ -1,4 +1,11 @@
sst.Linkable.wrap(random.RandomPassword, (resource) => ({
properties: {
value: resource.result,
},
}))
export const SECRET = {
R2AccessKey: new sst.Secret("R2AccessKey", "unknown"),
R2SecretKey: new sst.Secret("R2SecretKey", "unknown"),
HoneycombWebhookSecret: new random.RandomPassword("HoneycombWebhookSecret", { length: 24 }),
}

View File

@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-ynZFX8eCamzBuVpauYLbju/Cqbt2260JNumMUj79PKA=",
"aarch64-linux": "sha256-JCu7JZkdAAHTufWEJRV1gJErKvHFirq+qmVNIRPZ/0w=",
"aarch64-darwin": "sha256-9Dkt/poYBpLdtqA6L9pLe6GS435zFGb5rOYWE5rEnjA=",
"x86_64-darwin": "sha256-Nd5j28gAcM7+0ETBchjk9VojViHy3N/z2MkdU42YuCg="
"x86_64-linux": "sha256-Hw7sVV9rTm6qBMtdwfLIV2QvxvLQY5qrywXzuyYbhcs=",
"aarch64-linux": "sha256-++oXnY7YqrYt0Qv7ZISmoHliARM9qEP8FacqLxGZH1c=",
"aarch64-darwin": "sha256-kZVa0R1YbuvtTzpETqK6ddj4ISje5jBFHBdlynkhW7Q=",
"x86_64-darwin": "sha256-94eagNDa8GGJxF8BsMX2BF5Pa+QTl48lXL1+6HgEn0I="
}
}

View File

@@ -13,6 +13,7 @@
"dev:storybook": "bun --cwd packages/storybook storybook",
"lint": "oxlint",
"typecheck": "bun turbo typecheck",
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
"postinstall": "bun run --cwd packages/opencode fix-node-pty",
"prepare": "husky",
"random": "echo 'Random script'",
@@ -27,15 +28,16 @@
"packages/slack"
],
"catalog": {
"@effect/opentelemetry": "4.0.0-beta.57",
"@effect/platform-node": "4.0.0-beta.57",
"@effect/opentelemetry": "4.0.0-beta.65",
"@effect/platform-node": "4.0.0-beta.65",
"@npmcli/arborist": "9.4.0",
"@types/bun": "1.3.12",
"@types/cross-spawn": "6.0.6",
"@octokit/rest": "22.0.0",
"@hono/zod-validator": "0.4.2",
"@opentui/core": "0.2.2",
"@opentui/solid": "0.2.2",
"@opentui/core": "0.2.10",
"@opentui/keymap": "0.2.10",
"@opentui/solid": "0.2.10",
"ulid": "3.0.1",
"@kobalte/core": "0.13.11",
"@types/luxon": "3.7.1",
@@ -53,7 +55,7 @@
"dompurify": "3.3.1",
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
"effect": "4.0.0-beta.59",
"effect": "4.0.0-beta.65",
"ai": "6.0.168",
"cross-spawn": "7.0.6",
"hono": "4.10.7",
@@ -126,11 +128,15 @@
"electron"
],
"overrides": {
"@opentui/core": "catalog:",
"@opentui/keymap": "catalog:",
"@opentui/solid": "catalog:",
"@types/bun": "catalog:",
"@types/node": "catalog:"
},
"patchedDependencies": {
"@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch",
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.14.39",
"version": "1.14.51",
"description": "",
"type": "module",
"exports": {
@@ -73,7 +73,6 @@
"solid-js": "catalog:",
"solid-list": "catalog:",
"tailwindcss": "catalog:",
"virtua": "catalog:",
"zod": "catalog:"
"virtua": "catalog:"
}
}

View File

@@ -107,7 +107,8 @@ function createCommandEntries(props: {
const allowed = createMemo(() => {
if (props.filesOnly()) return []
return props.command.options.filter(
(option) => !option.disabled && !option.id.startsWith("suggested.") && option.id !== "file.open",
(option) =>
!option.disabled && !option.hidden && !option.id.startsWith("suggested.") && option.id !== "file.open",
)
})

View File

@@ -6,12 +6,14 @@ import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
import { Switch } from "@opencode-ai/ui/switch"
import { useLanguage } from "@/context/language"
import { mcpQueryKey } from "@/context/global-sync"
import { useQueryOptions } from "@/context/global-sync"
import { pathKey } from "@/utils/path-key"
const statusLabels = {
connected: "mcp.status.connected",
failed: "mcp.status.failed",
needs_auth: "mcp.status.needs_auth",
needs_client_registration: "mcp.status.needs_client_registration",
disabled: "mcp.status.disabled",
} as const
@@ -20,6 +22,7 @@ export const DialogSelectMcp: Component = () => {
const sdk = useSDK()
const language = useLanguage()
const queryClient = useQueryClient()
const queryOptions = useQueryOptions()
const items = createMemo(() =>
Object.entries(sync.data.mcp ?? {})
@@ -29,10 +32,18 @@ export const DialogSelectMcp: Component = () => {
const toggle = useMutation(() => ({
mutationFn: async (name: string) => {
if (sync.data.mcp[name]?.status === "connected") await sdk.client.mcp.disconnect({ name })
else await sdk.client.mcp.connect({ name })
const status = sync.data.mcp[name]
if (status?.status === "connected") {
await sdk.client.mcp.disconnect({ name })
return
}
if (status?.status === "needs_auth") {
await sdk.client.mcp.auth.authenticate({ name })
return
}
await sdk.client.mcp.connect({ name })
},
onSuccess: () => queryClient.refetchQueries({ queryKey: mcpQueryKey(sync.directory) }),
onSuccess: () => queryClient.refetchQueries(queryOptions.mcp(pathKey(sync.directory))),
}))
const enabledCount = createMemo(() => items().filter((i) => i.status === "connected").length)
@@ -65,7 +76,7 @@ export const DialogSelectMcp: Component = () => {
}
const error = () => {
const s = mcpStatus()
return s?.status === "failed" ? s.error : undefined
if (s?.status === "failed" || s?.status === "needs_client_registration") return s.error
}
const enabled = () => status() === "connected"
return (
@@ -76,9 +87,6 @@ export const DialogSelectMcp: Component = () => {
<Show when={statusLabel()}>
<span class="text-11-regular text-text-weaker">{statusLabel()}</span>
</Show>
<Show when={toggle.isPending && toggle.variables === i.name}>
<span class="text-11-regular text-text-weak">{language.t("common.loading.ellipsis")}</span>
</Show>
</div>
<Show when={error()}>
<span class="text-11-regular text-text-weaker truncate">{error()}</span>

View File

@@ -16,7 +16,6 @@ import {
} from "@/context/prompt"
import { useLayout } from "@/context/layout"
import { useSDK } from "@/context/sdk"
import { useGlobalSDK } from "@/context/global-sdk"
import { useSync } from "@/context/sync"
import { useComments } from "@/context/comments"
import { Button } from "@opencode-ai/ui/button"
@@ -56,7 +55,8 @@ import { PromptDragOverlay } from "./prompt-input/drag-overlay"
import { promptPlaceholder } from "./prompt-input/placeholder"
import { ImagePreview } from "@opencode-ai/ui/image-preview"
import { useQueries } from "@tanstack/solid-query"
import { loadAgentsQuery, loadProvidersQuery } from "@/context/global-sync/bootstrap"
import { useQueryOptions } from "@/context/global-sync"
import { pathKey } from "@/utils/path-key"
interface PromptInputProps {
class?: string
@@ -103,7 +103,7 @@ const NON_EMPTY_TEXT = /[^\s\u200B]/
export const PromptInput: Component<PromptInputProps> = (props) => {
const sdk = useSDK()
const globalSDK = useGlobalSDK()
const queryOptions = useQueryOptions()
const sync = useSync()
const local = useLocal()
@@ -240,13 +240,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
return paths
})
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
const status = createMemo(
() =>
sync.data.session_status[params.id ?? ""] ?? {
type: "idle",
},
)
const working = createMemo(() => status()?.type !== "idle")
const working = createMemo(() => sync.data.session_working(params.id ?? ""))
const imageAttachments = createMemo(() =>
prompt.current().filter((part): part is ImageAttachmentPart => part.type === "image"),
)
@@ -1256,9 +1250,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const [agentsQuery, globalProvidersQuery, providersQuery] = useQueries(() => ({
queries: [
loadAgentsQuery(sdk.directory, sdk.client),
loadProvidersQuery(null, globalSDK.client),
loadProvidersQuery(sdk.directory, sdk.client),
queryOptions.agents(pathKey(sdk.directory)),
queryOptions.providers(null),
queryOptions.providers(pathKey(sdk.directory)),
],
}))

View File

@@ -123,11 +123,13 @@ function listFor(command: CommandContext, map: KeybindMap, palette: string) {
for (const opt of command.catalog) {
if (opt.id.startsWith("suggested.")) continue
if (opt.hidden) continue
out.set(opt.id, { title: opt.title, group: groupFor(opt.id) })
}
for (const opt of command.options) {
if (opt.id.startsWith("suggested.")) continue
if (opt.hidden) continue
out.set(opt.id, { title: opt.title, group: groupFor(opt.id) })
}

View File

@@ -15,7 +15,8 @@ import { useSDK } from "@/context/sdk"
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
import { useSync } from "@/context/sync"
import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health"
import { mcpQueryKey } from "@/context/global-sync"
import { useQueryOptions } from "@/context/global-sync"
import { pathKey } from "@/utils/path-key"
const pollMs = 10_000
@@ -139,13 +140,22 @@ const useMcpToggleMutation = () => {
const sdk = useSDK()
const language = useLanguage()
const queryClient = useQueryClient()
const queryOptions = useQueryOptions()
return useMutation(() => ({
mutationFn: async (name: string) => {
const status = sync.data.mcp[name]
await (status?.status === "connected" ? sdk.client.mcp.disconnect({ name }) : sdk.client.mcp.connect({ name }))
if (status?.status === "connected") {
await sdk.client.mcp.disconnect({ name })
return
}
if (status?.status === "needs_auth") {
await sdk.client.mcp.auth.authenticate({ name })
return
}
await sdk.client.mcp.connect({ name })
},
onSuccess: () => queryClient.refetchQueries({ queryKey: mcpQueryKey(sync.directory) }),
onSuccess: () => queryClient.refetchQueries(queryOptions.mcp(pathKey(sync.directory))),
onError: (err) => {
showToast({
variant: "error",
@@ -314,7 +324,7 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
return (
<button
type="button"
class="flex items-center gap-2 w-full h-8 pl-3 pr-2 py-1 rounded-md hover:bg-surface-raised-base-hover transition-colors text-left"
class="flex items-center gap-2 w-full min-h-8 pl-3 pr-2 py-1 rounded-md hover:bg-surface-raised-base-hover transition-colors text-left"
onClick={() => {
if (toggleMcp.isPending) return
toggleMcp.mutate(name)
@@ -331,7 +341,16 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
status() === "needs_auth" || status() === "needs_client_registration",
}}
/>
<span class="text-14-regular text-text-base truncate flex-1">{name}</span>
<span class="flex flex-col min-w-0 flex-1">
<span class="flex items-center gap-2 min-w-0">
<span class="text-14-regular text-text-base truncate">{name}</span>
</span>
<Show when={status() === "needs_auth"}>
<span class="text-11-regular text-text-weaker truncate">
{language.t("mcp.auth.clickToAuthenticate")}
</span>
</Show>
</span>
<div onClick={(event) => event.stopPropagation()}>
<Switch
checked={enabled()}

View File

@@ -14,12 +14,14 @@ export function StatusPopover() {
const sync = useSync()
const [shown, setShown] = createSignal(false)
const ready = createMemo(() => server.healthy() === false || sync.data.mcp_ready)
const healthy = createMemo(() => {
const serverHealthy = server.healthy() === true
const mcpIssue = createMemo(() => {
const mcp = Object.values(sync.data.mcp ?? {})
const issue = mcp.some((item) => item.status !== "connected" && item.status !== "disabled")
return serverHealthy && !issue
const failed = mcp.some((item) => item.status === "failed" || item.status === "needs_client_registration")
const warn = mcp.some((item) => item.status === "needs_auth")
if (failed) return "critical" as const
if (warn) return "warning" as const
})
const healthy = createMemo(() => server.healthy() === true && !mcpIssue())
return (
<Popover
@@ -41,7 +43,9 @@ export function StatusPopover() {
classList={{
"absolute -top-px -right-px size-1.5 rounded-full": true,
"bg-icon-success-base": ready() && healthy(),
"bg-icon-critical-base": server.healthy() === false || (ready() && !healthy()),
"bg-icon-warning-base": ready() && server.healthy() === true && mcpIssue() === "warning",
"bg-icon-critical-base":
server.healthy() === false || (ready() && server.healthy() === true && mcpIssue() === "critical"),
"bg-border-weak-base": server.healthy() === undefined || !ready(),
}}
/>

View File

@@ -81,6 +81,7 @@ export interface CommandOption {
slash?: string
suggested?: boolean
disabled?: boolean
hidden?: boolean
onSelect?: (source?: "palette" | "keybind" | "slash") => void
onHighlight?: () => (() => void) | void
}
@@ -93,6 +94,7 @@ export type CommandCatalogItem = {
category?: string
keybind?: KeybindConfig
slash?: string
hidden?: boolean
}
export type CommandRegistration = {
@@ -279,13 +281,14 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
setCatalog(
registered().reduce((acc, opt) => {
const id = actionId(opt.id)
acc[id] = {
title: opt.title,
description: opt.description,
category: opt.category,
keybind: opt.keybind,
slash: opt.slash,
}
if (opt.title)
acc[id] = {
title: opt.title,
description: opt.description,
category: opt.category,
keybind: opt.keybind,
slash: opt.slash,
}
return acc
}, {} as CommandCatalog),
)

View File

@@ -3,15 +3,13 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { makeEventListener } from "@solid-primitives/event-listener"
import { batch, onCleanup, onMount } from "solid-js"
import z from "zod"
import { createSdkForServer } from "@/utils/server"
import { useLanguage } from "./language"
import { usePlatform } from "./platform"
import { useServer } from "./server"
const abortError = z.object({
name: z.literal("AbortError"),
})
const isAbortError = (error: unknown) =>
error !== null && typeof error === "object" && "name" in error && error.name === "AbortError"
export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({
name: "GlobalSDK",
@@ -103,7 +101,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
let streamErrorLogged = false
const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
const aborted = (error: unknown) => abortError.safeParse(error).success
const aborted = isAbortError
let attempt: AbortController | undefined
let run: Promise<void> | undefined

View File

@@ -18,8 +18,10 @@ import {
bootstrapDirectory,
bootstrapGlobal,
clearProviderRev,
loadAgentsQuery,
loadGlobalConfigQuery,
loadPathQuery,
loadProjectsQuery,
loadProvidersQuery,
} from "./global-sync/bootstrap"
import { createChildStoreManager } from "./global-sync/child-store"
@@ -33,6 +35,7 @@ import { formatServerError } from "@/utils/server-errors"
import { queryOptions, useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/solid-query"
import { createRefreshQueue } from "./global-sync/queue"
import { directoryKey } from "./global-sync/utils"
import { PathKey } from "@/utils/path-key"
type GlobalStore = {
ready: boolean
@@ -48,24 +51,33 @@ type GlobalStore = {
reload: undefined | "pending" | "complete"
}
export const loadSessionsQueryKey = (directory: string) => [directory, "loadSessions"] as const
export const mcpQueryKey = (directory: string) => [directory, "mcp"] as const
export const loadMcpQuery = (directory: string, sdk: OpencodeClient) =>
queryOptions({
queryKey: mcpQueryKey(directory),
queryKey: [directory, "mcp"] as const,
queryFn: () => sdk.mcp.status().then((r) => r.data ?? {}),
})
export const lspQueryKey = (directory: string) => [directory, "lsp"] as const
export const loadLspQuery = (directory: string, sdk: OpencodeClient) =>
queryOptions({
queryKey: lspQueryKey(directory),
queryKey: [directory, "lsp"] as const,
queryFn: () => sdk.lsp.status().then((r) => r.data ?? []),
})
function makeQueryOptionsApi(globalSDK: () => OpencodeClient, sdkFor: (dir: PathKey) => OpencodeClient) {
return {
globalConfig: () => loadGlobalConfigQuery(globalSDK()),
projects: () => loadProjectsQuery(globalSDK()),
providers: (directory: PathKey | null) =>
loadProvidersQuery(directory, directory === null ? globalSDK() : sdkFor(directory)),
path: (directory: PathKey | null) => loadPathQuery(directory, directory === null ? globalSDK() : sdkFor(directory)),
agents: (directory: PathKey) => loadAgentsQuery(directory, sdkFor(directory)),
mcp: (directory: PathKey) => loadMcpQuery(directory, sdkFor(directory)),
lsp: (directory: PathKey) => loadLspQuery(directory, sdkFor(directory)),
sessions: (directory: PathKey) => ({ queryKey: [directory, "loadSessions"] as const }),
}
}
export type QueryOptionsApi = ReturnType<typeof makeQueryOptionsApi>
function createGlobalSync() {
const globalSDK = useGlobalSDK()
const language = useLanguage()
@@ -77,12 +89,22 @@ function createGlobalSync() {
const sessionLoads = new Map<string, Promise<void>>()
const sessionMeta = new Map<string, { limit: number }>()
const sdkFor = (directory: string) => {
const key = directoryKey(directory)
const cached = sdkCache.get(key)
if (cached) return cached
const sdk = globalSDK.createClient({
directory,
throwOnError: true,
})
sdkCache.set(key, sdk)
return sdk
}
const queryOptionsApi = makeQueryOptionsApi(() => globalSDK.client, sdkFor)
const [configQuery, providerQuery, pathQuery] = useQueries(() => ({
queries: [
loadGlobalConfigQuery(globalSDK.client),
loadProvidersQuery(null, globalSDK.client),
loadPathQuery(null, globalSDK.client),
],
queries: [queryOptionsApi.globalConfig(), queryOptionsApi.providers(null), queryOptionsApi.path(null)],
}))
const [globalStore, setGlobalStore] = createStore<GlobalStore>({
@@ -181,18 +203,6 @@ function createGlobalSync() {
bootstrapInstance,
})
const sdkFor = (directory: string) => {
const key = directoryKey(directory)
const cached = sdkCache.get(key)
if (cached) return cached
const sdk = globalSDK.createClient({
directory,
throwOnError: true,
})
sdkCache.set(key, sdk)
return sdk
}
const children = createChildStoreManager({
owner,
isBooting: (directory) => booting.has(directory),
@@ -209,7 +219,7 @@ function createGlobalSync() {
clearSessionPrefetchDirectory(key)
},
translate: language.t,
getSdk: sdkFor,
queryOptions: queryOptionsApi,
global: {
provider: globalStore.provider,
},
@@ -239,7 +249,7 @@ function createGlobalSync() {
const limit = Math.max(store.limit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT)
const promise = queryClient
.fetchQuery({
queryKey: loadSessionsQueryKey(key),
...queryOptionsApi.sessions(key),
queryFn: () =>
loadRootSessionsWithFallback({
directory,
@@ -368,7 +378,7 @@ function createGlobalSync() {
setSessionTodo,
vcsCache: children.vcsCache.get(key),
loadLsp: () => {
void queryClient.fetchQuery(loadLspQuery(key, sdkFor(directory)))
void queryClient.fetchQuery(queryOptionsApi.lsp(key))
},
})
})
@@ -426,6 +436,7 @@ function createGlobalSync() {
},
child: children.child,
peek: children.peek,
queryOptions: queryOptionsApi,
// bootstrap,
updateConfig: updateConfigMutation.mutateAsync,
project: projectApi,
@@ -447,3 +458,7 @@ export function useGlobalSync() {
if (!context) throw new Error("useGlobalSync must be used within GlobalSyncProvider")
return context
}
export function useQueryOptions() {
return useGlobalSync().queryOptions
}

View File

@@ -22,7 +22,7 @@ describe("createChildStoreManager", () => {
onBootstrap() {},
onDispose() {},
translate: (key) => key,
getSdk: () => null!,
queryOptions: {} as any,
global: { provider: null! },
})

View File

@@ -1,7 +1,7 @@
import { createRoot, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js"
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
import { Persist, persisted } from "@/utils/persist"
import type { OpencodeClient, ProviderListResponse, VcsInfo } from "@opencode-ai/sdk/v2/client"
import type { ProviderListResponse, VcsInfo } from "@opencode-ai/sdk/v2/client"
import {
DIR_IDLE_TTL_MS,
MAX_DIR_STORES,
@@ -15,8 +15,7 @@ import {
} from "./types"
import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
import { useQueries } from "@tanstack/solid-query"
import { loadPathQuery, loadProvidersQuery } from "./bootstrap"
import { loadLspQuery, loadMcpQuery } from "../global-sync"
import { QueryOptionsApi } from "../global-sync"
import { directoryKey, type DirectoryKey } from "./utils"
export function createChildStoreManager(input: {
@@ -26,7 +25,7 @@ export function createChildStoreManager(input: {
onBootstrap: (directory: string) => void
onDispose: (directory: string) => void
translate: (key: string, vars?: Record<string, string | number>) => string
getSdk: (directory: string) => OpencodeClient
queryOptions: QueryOptionsApi
global: {
provider: ProviderListResponse
}
@@ -171,17 +170,15 @@ export function createChildStoreManager(input: {
const init = () =>
createRoot((dispose) => {
const sdk = input.getSdk(directory)
const initialMeta = meta[0].value
const initialIcon = icon[0].value
const [pathQuery, mcpQuery, lspQuery, providerQuery] = useQueries(() => ({
queries: [
loadPathQuery(key, sdk),
loadMcpQuery(key, sdk),
loadLspQuery(key, sdk),
loadProvidersQuery(key, sdk),
input.queryOptions.path(key),
input.queryOptions.mcp(key),
input.queryOptions.lsp(key),
input.queryOptions.providers(key),
],
}))
@@ -211,6 +208,10 @@ export function createChildStoreManager(input: {
session: [],
sessionTotal: 0,
session_status: {},
session_working(id: string) {
const type = this.session_status[id]?.type
return (type ?? "idle") !== "idle"
},
session_diff: {},
todo: {},
permission: {},
@@ -231,6 +232,7 @@ export function createChildStoreManager(input: {
limit: 5,
message: {},
part: {},
part_text_accum_delta: {},
})
children[key] = child
disposers.set(key, dispose)

View File

@@ -81,6 +81,7 @@ const baseState = (input: Partial<State> = {}) =>
limit: 10,
message: {},
part: {},
part_text_accum_delta: {},
...input,
}) as State

View File

@@ -211,6 +211,12 @@ export function applyDirectoryEvent(input: {
const result = Binary.search(messages, props.messageID, (m) => m.id)
if (result.found) messages.splice(result.index, 1)
}
const parts = draft.part[props.messageID]
if (parts) {
for (const part of parts) {
delete draft.part_text_accum_delta[part.id]
}
}
delete draft.part[props.messageID]
}),
)
@@ -219,6 +225,11 @@ export function applyDirectoryEvent(input: {
case "message.part.updated": {
const part = (event.properties as { part: Part }).part
if (SKIP_PARTS.has(part.type)) break
input.setStore(
produce((draft) => {
delete draft.part_text_accum_delta[part.id]
}),
)
const parts = input.store.part[part.messageID]
if (!parts) {
input.setStore("part", part.messageID, [part])
@@ -240,6 +251,11 @@ export function applyDirectoryEvent(input: {
}
case "message.part.removed": {
const props = event.properties as { messageID: string; partID: string }
input.setStore(
produce((draft) => {
delete draft.part_text_accum_delta[props.partID]
}),
)
const parts = input.store.part[props.messageID]
if (!parts) break
const result = Binary.search(parts, props.partID, (p) => p.id)
@@ -263,6 +279,7 @@ export function applyDirectoryEvent(input: {
if (!parts) break
const result = Binary.search(parts, props.partID, (p) => p.id)
if (!result.found) break
input.setStore("part_text_accum_delta", props.partID, (existing) => (existing ?? "") + props.delta)
input.setStore(
"part",
props.messageID,

View File

@@ -39,6 +39,7 @@ describe("app session cache", () => {
part: Record<string, Part[] | undefined>
permission: Record<string, PermissionRequest[] | undefined>
question: Record<string, QuestionRequest[] | undefined>
part_text_accum_delta: Record<string, string | undefined>
} = {
session_status: { ses_1: { type: "busy" } as SessionStatus },
session_diff: { ses_1: [] },
@@ -47,12 +48,14 @@ describe("app session cache", () => {
part: { msg_1: [part("prt_1", "ses_1", "msg_1")] },
permission: { ses_1: [] as PermissionRequest[] },
question: { ses_1: [] as QuestionRequest[] },
part_text_accum_delta: { prt_1: "streamed text" },
}
dropSessionCaches(store, ["ses_1"])
expect(store.message.ses_1).toBeUndefined()
expect(store.part.msg_1).toBeUndefined()
expect(store.part_text_accum_delta.prt_1).toBeUndefined()
expect(store.todo.ses_1).toBeUndefined()
expect(store.session_diff.ses_1).toBeUndefined()
expect(store.session_status.ses_1).toBeUndefined()
@@ -70,6 +73,7 @@ describe("app session cache", () => {
part: Record<string, Part[] | undefined>
permission: Record<string, PermissionRequest[] | undefined>
question: Record<string, QuestionRequest[] | undefined>
part_text_accum_delta: Record<string, string | undefined>
} = {
session_status: {},
session_diff: {},
@@ -78,6 +82,7 @@ describe("app session cache", () => {
part: { [m.id]: [part("prt_1", "ses_1", m.id)] },
permission: {},
question: {},
part_text_accum_delta: {},
}
dropSessionCaches(store, ["ses_1"])

View File

@@ -18,6 +18,7 @@ type SessionCache = {
part: Record<string, Part[] | undefined>
permission: Record<string, PermissionRequest[] | undefined>
question: Record<string, QuestionRequest[] | undefined>
part_text_accum_delta: Record<string, string | undefined>
}
export function dropSessionCaches(store: SessionCache, sessionIDs: Iterable<string>) {
@@ -27,6 +28,9 @@ export function dropSessionCaches(store: SessionCache, sessionIDs: Iterable<stri
for (const key of Object.keys(store.part)) {
const parts = store.part[key]
if (!parts?.some((part) => stale.has(part?.sessionID ?? ""))) continue
for (const part of parts) {
delete store.part_text_accum_delta[part.id]
}
delete store.part[key]
}

View File

@@ -46,6 +46,7 @@ export type State = {
session_status: {
[sessionID: string]: SessionStatus
}
session_working(id: string): boolean
session_diff: {
[sessionID: string]: SnapshotFileDiff[]
}
@@ -72,6 +73,9 @@ export type State = {
part: {
[messageID: string]: Part[]
}
part_text_accum_delta: {
[partID: string]: string
}
}
export type VcsCache = {

View File

@@ -43,6 +43,7 @@ type SessionView = {
reviewOpen?: string[]
pendingMessage?: string
pendingMessageAt?: number
todoCollapsed?: boolean
}
type TabHandoff = {
@@ -759,6 +760,18 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setScroll(tab: string, pos: SessionScroll) {
scroll.setScroll(key(), tab, pos)
},
todoCollapsed: {
get: () => s().todoCollapsed ?? false,
set(collapsed: boolean) {
const session = key()
const current = store.sessionView[session]
if (!current) {
setStore("sessionView", session, { scroll: {}, todoCollapsed: collapsed })
} else {
setStore("sessionView", session, "todoCollapsed", collapsed)
}
},
},
terminal: {
opened: terminalOpened,
open() {

View File

@@ -44,7 +44,7 @@ const migrate = (value: unknown) => {
}
const clone = (value: State | undefined) => {
if (!value) return undefined
if (!value) return
return {
...value,
model: value.model ? { ...value.model } : undefined,
@@ -104,7 +104,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const pickAgent = (name: string | undefined) => {
const items = list()
if (items.length === 0) return undefined
if (items.length === 0) return
return items.find((item) => item.name === name) ?? items[0]
}
@@ -227,14 +227,14 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
() => agent.current()?.model,
fallback,
)
if (!item) return undefined
if (!item) return
return models.find(item)
}
const configured = () => {
const item = agent.current()
const model = current()
if (!item || !model) return undefined
if (!item || !model) return
return getConfiguredAgentVariant({
agent: { model: item.model, variant: item.variant },
model: { providerID: model.provider.id, modelID: model.id, variants: model.variants },
@@ -314,11 +314,16 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
configured,
selected,
current() {
return resolveModelVariant({
const resolved = resolveModelVariant({
variants: this.list(),
selected: this.selected(),
configured: this.configured(),
})
if (resolved) return resolved
const model = current()
if (!model) return
const saved = models.variant.get({ providerID: model.provider.id, modelID: model.id })
if (saved && this.list().includes(saved)) return saved
},
list() {
const item = current()
@@ -335,6 +340,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
variant: value ?? null,
})
write({ variant: value ?? null })
if (model) {
models.variant.set({ providerID: model.provider.id, modelID: model.id }, value ?? undefined)
}
})
},
cycle() {

View File

@@ -276,6 +276,7 @@ export const dict = {
"mcp.status.connected": "متصل",
"mcp.status.failed": "فشل",
"mcp.status.needs_auth": "يحتاج إلى مصادقة",
"mcp.auth.clickToAuthenticate": "انقر للمصادقة",
"mcp.status.disabled": "معطل",
"dialog.fork.empty": "لا توجد رسائل للتفرع منها",
"dialog.directory.search.placeholder": "البحث في المجلدات",

View File

@@ -276,6 +276,7 @@ export const dict = {
"mcp.status.connected": "conectado",
"mcp.status.failed": "falhou",
"mcp.status.needs_auth": "precisa de autenticação",
"mcp.auth.clickToAuthenticate": "Clique para autenticar",
"mcp.status.disabled": "desabilitado",
"dialog.fork.empty": "Nenhuma mensagem para bifurcar",
"dialog.directory.search.placeholder": "Buscar pastas",

View File

@@ -300,6 +300,7 @@ export const dict = {
"mcp.status.connected": "povezano",
"mcp.status.failed": "neuspjelo",
"mcp.status.needs_auth": "potrebna autentifikacija",
"mcp.auth.clickToAuthenticate": "Kliknite za autentifikaciju",
"mcp.status.disabled": "onemogućeno",
"dialog.fork.empty": "Nema poruka za fork",

View File

@@ -298,6 +298,7 @@ export const dict = {
"mcp.status.connected": "forbundet",
"mcp.status.failed": "mislykkedes",
"mcp.status.needs_auth": "kræver godkendelse",
"mcp.auth.clickToAuthenticate": "Klik for at godkende",
"mcp.status.disabled": "deaktiveret",
"dialog.fork.empty": "Ingen beskeder at forgrene fra",

View File

@@ -282,6 +282,7 @@ export const dict = {
"mcp.status.connected": "verbunden",
"mcp.status.failed": "fehlgeschlagen",
"mcp.status.needs_auth": "benötigt Authentifizierung",
"mcp.auth.clickToAuthenticate": "Zum Authentifizieren klicken",
"mcp.status.disabled": "deaktiviert",
"dialog.fork.empty": "Keine Nachrichten zum Abzweigen vorhanden",
"dialog.directory.search.placeholder": "Ordner durchsuchen",

View File

@@ -25,6 +25,7 @@ export const dict = {
"command.project.open": "Open project",
"command.project.previous": "Previous project",
"command.project.next": "Next project",
"command.project.index": "Switch to project {{index}}",
"command.provider.connect": "Connect provider",
"command.server.switch": "Switch server",
"command.settings.open": "Open settings",
@@ -305,6 +306,7 @@ export const dict = {
"mcp.status.failed": "failed",
"mcp.status.needs_auth": "needs auth",
"mcp.status.disabled": "disabled",
"mcp.auth.clickToAuthenticate": "Click to authenticate",
"dialog.fork.empty": "No messages to fork from",
@@ -901,7 +903,7 @@ export const dict = {
"settings.permissions.tool.read.title": "Read",
"settings.permissions.tool.read.description": "Reading a file (matches the file path)",
"settings.permissions.tool.edit.title": "Edit",
"settings.permissions.tool.edit.description": "Modify files, including edits, writes, patches, and multi-edits",
"settings.permissions.tool.edit.description": "Modify files, including edits, writes, and patches",
"settings.permissions.tool.glob.title": "Glob",
"settings.permissions.tool.glob.description": "Match files using glob patterns",
"settings.permissions.tool.grep.title": "Grep",

View File

@@ -299,6 +299,7 @@ export const dict = {
"mcp.status.connected": "conectado",
"mcp.status.failed": "fallido",
"mcp.status.needs_auth": "necesita auth",
"mcp.auth.clickToAuthenticate": "Haz clic para autenticar",
"mcp.status.disabled": "deshabilitado",
"dialog.fork.empty": "No hay mensajes desde donde bifurcar",

View File

@@ -277,6 +277,7 @@ export const dict = {
"mcp.status.connected": "connecté",
"mcp.status.failed": "échoué",
"mcp.status.needs_auth": "nécessite auth",
"mcp.auth.clickToAuthenticate": "Cliquez pour vous authentifier",
"mcp.status.disabled": "désactivé",
"dialog.fork.empty": "Aucun message à partir duquel bifurquer",
"dialog.directory.search.placeholder": "Rechercher des dossiers",

View File

@@ -275,6 +275,7 @@ export const dict = {
"mcp.status.connected": "接続済み",
"mcp.status.failed": "失敗",
"mcp.status.needs_auth": "認証が必要",
"mcp.auth.clickToAuthenticate": "クリックして認証",
"mcp.status.disabled": "無効",
"dialog.fork.empty": "フォーク元のメッセージがありません",
"dialog.directory.search.placeholder": "フォルダを検索",

View File

@@ -275,6 +275,7 @@ export const dict = {
"mcp.status.connected": "연결됨",
"mcp.status.failed": "실패",
"mcp.status.needs_auth": "인증 필요",
"mcp.auth.clickToAuthenticate": "클릭하여 인증",
"mcp.status.disabled": "비활성화됨",
"dialog.fork.empty": "분기할 메시지 없음",
"dialog.directory.search.placeholder": "폴더 검색",

View File

@@ -302,6 +302,7 @@ export const dict = {
"mcp.status.connected": "tilkoblet",
"mcp.status.failed": "mislyktes",
"mcp.status.needs_auth": "trenger autentisering",
"mcp.auth.clickToAuthenticate": "Klikk for å autentisere",
"mcp.status.disabled": "deaktivert",
"dialog.fork.empty": "Ingen meldinger å forgrene fra",

View File

@@ -277,6 +277,7 @@ export const dict = {
"mcp.status.connected": "połączono",
"mcp.status.failed": "niepowodzenie",
"mcp.status.needs_auth": "wymaga autoryzacji",
"mcp.auth.clickToAuthenticate": "Kliknij, aby się uwierzytelnić",
"mcp.status.disabled": "wyłączone",
"dialog.fork.empty": "Brak wiadomości do rozwidlenia",
"dialog.directory.search.placeholder": "Szukaj folderów",

View File

@@ -299,6 +299,7 @@ export const dict = {
"mcp.status.connected": "подключено",
"mcp.status.failed": "ошибка",
"mcp.status.needs_auth": "требуется авторизация",
"mcp.auth.clickToAuthenticate": "Нажмите, чтобы авторизоваться",
"mcp.status.disabled": "отключено",
"dialog.fork.empty": "Нет сообщений для ответвления",

View File

@@ -299,6 +299,7 @@ export const dict = {
"mcp.status.connected": "เชื่อมต่อแล้ว",
"mcp.status.failed": "ล้มเหลว",
"mcp.status.needs_auth": "ต้องการการตรวจสอบสิทธิ์",
"mcp.auth.clickToAuthenticate": "คลิกเพื่อยืนยันตัวตน",
"mcp.status.disabled": "ปิดใช้งาน",
"dialog.fork.empty": "ไม่มีข้อความให้แตกแขนง",

View File

@@ -304,6 +304,7 @@ export const dict = {
"mcp.status.connected": "bağlı",
"mcp.status.failed": "başarısız",
"mcp.status.needs_auth": "kimlik doğrulama gerekli",
"mcp.auth.clickToAuthenticate": "Kimlik doğrulamak için tıklayın",
"mcp.status.disabled": "devre dışı",
"dialog.fork.empty": "Dallandırılacak mesaj yok",

View File

@@ -319,6 +319,7 @@ export const dict = {
"mcp.status.connected": "已连接",
"mcp.status.failed": "失败",
"mcp.status.needs_auth": "需要授权",
"mcp.auth.clickToAuthenticate": "点击进行授权",
"mcp.status.disabled": "已禁用",
"dialog.fork.empty": "没有可用于分叉的消息",

View File

@@ -299,6 +299,7 @@ export const dict = {
"mcp.status.connected": "已連線",
"mcp.status.failed": "失敗",
"mcp.status.needs_auth": "需要授權",
"mcp.auth.clickToAuthenticate": "點擊以進行授權",
"mcp.status.disabled": "已停用",
"dialog.fork.empty": "沒有可用於分支的訊息",

View File

@@ -960,6 +960,15 @@ export default function Layout(props: ParentProps) {
void openProject(target.worktree)
}
function navigateToProjectIndex(index: number) {
const projects = layout.projects.list()
const target = projects[index]
if (!target) return
globalSync.child(target.worktree)
void openProject(target.worktree)
}
function navigateSessionByUnseen(offset: number) {
const sessions = currentSessions()
if (sessions.length === 0) return
@@ -1040,6 +1049,19 @@ export default function Layout(props: ParentProps) {
keybind: "mod+alt+arrowdown",
onSelect: () => navigateProjectByOffset(1),
},
...Array.from({ length: 9 }, (_, i) => {
const index = i
const number = index + 1
return {
id: `project.${number}`,
category: language.t("command.category.project"),
title: `Open Project {number}`,
keybind: `mod+${number}`,
disabled: layout.projects.list().length <= index,
hidden: true,
onSelect: () => navigateToProjectIndex(index),
}
}),
{
id: "provider.connect",
title: language.t("command.provider.connect"),
@@ -1409,19 +1431,20 @@ export default function Layout(props: ParentProps) {
const index = list.findIndex((x) => pathKey(x.worktree) === key)
const active = pathKey(currentProject()?.worktree ?? "") === key
if (index === -1) return
const next = list[index + 1]
if (!active) {
layout.projects.close(directory)
return
}
if (!next) {
if (list.length === 1) {
layout.projects.close(directory)
navigate("/")
return
}
const next = list[index + 1] ?? list[index - 1]
navigateWithSidebarReset(`/${base64Encode(next.worktree)}/session`)
layout.projects.close(directory)
queueMicrotask(() => {
@@ -1934,7 +1957,7 @@ export default function Layout(props: ParentProps) {
if (!created?.directory) return
setWorkspaceName(created.directory, created.branch, project.id, created.branch)
setWorkspaceName(created.directory, created.branch ?? getFilename(created.directory), project.id, created.branch)
const local = project.worktree
const key = pathKey(created.directory)
@@ -2096,6 +2119,7 @@ export default function Layout(props: ParentProps) {
</div>
</Show>
}
keyed
>
{(project) => (
<>
@@ -2106,9 +2130,7 @@ export default function Layout(props: ParentProps) {
id={`project:${projectId()}`}
value={projectName}
onSave={(next) => {
const item = project()
if (!item) return
void renameProject(item, next)
void renameProject(project, next)
}}
class="text-14-medium text-text-strong truncate"
displayClass="text-14-medium text-text-strong truncate"
@@ -2150,9 +2172,7 @@ export default function Layout(props: ParentProps) {
<DropdownMenu.Content class="mt-1">
<DropdownMenu.Item
onSelect={() => {
const item = project()
if (!item) return
showEditProjectDialog(item)
showEditProjectDialog(project)
}}
>
<DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
@@ -2162,9 +2182,7 @@ export default function Layout(props: ParentProps) {
data-project={slug()}
disabled={!canToggle()}
onSelect={() => {
const item = project()
if (!item) return
toggleProjectWorkspaces(item)
toggleProjectWorkspaces(project)
}}
>
<DropdownMenu.ItemLabel>
@@ -2223,7 +2241,7 @@ export default function Layout(props: ParentProps) {
<div class="flex-1 min-h-0">
<LocalWorkspace
ctx={workspaceSidebarCtx}
project={project()}
project={project}
sortNow={sortNow}
mobile={panelProps.mobile}
/>
@@ -2238,9 +2256,7 @@ export default function Layout(props: ParentProps) {
icon="plus-small"
class="w-full"
onClick={() => {
const item = project()
if (!item) return
void createWorkspace(item)
void createWorkspace(project)
}}
>
{language.t("workspace.new")}
@@ -2267,7 +2283,7 @@ export default function Layout(props: ParentProps) {
<SortableWorkspace
ctx={workspaceSidebarCtx}
directory={directory}
project={project()}
project={project}
sortNow={sortNow}
mobile={panelProps.mobile}
/>

View File

@@ -26,7 +26,12 @@ export function getProjectAvatarSource(id?: string, icon?: { color?: string; url
return icon?.url
}
export const ProjectIcon = (props: { project: LocalProject; class?: string; notify?: boolean }): JSX.Element => {
export const ProjectIcon = (props: {
project: LocalProject
class?: string
notify?: boolean
working?: boolean
}): JSX.Element => {
const globalSync = useGlobalSync()
const notification = useNotification()
const permission = usePermission()
@@ -65,6 +70,11 @@ export const ProjectIcon = (props: { project: LocalProject; class?: string; noti
}}
/>
</Show>
<Show when={props.working}>
<div class="absolute bottom-px right-px size-3 rounded-full bg-background-base z-10 flex items-center justify-center">
<Spinner class="size-[9px]" />
</div>
</Show>
</div>
)
}
@@ -156,18 +166,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
})
const isWorking = createMemo(() => {
if (hasPermissions()) return false
const pending = (sessionStore.message[props.session.id] ?? []).findLast(
(message) =>
message.role === "assistant" &&
typeof (message as { time?: { completed?: unknown } }).time?.completed !== "number",
)
const status = sessionStore.session_status[props.session.id]
return (
pending !== undefined ||
status?.type === "busy" ||
status?.type === "retry" ||
(status !== undefined && status.type !== "idle")
)
return sessionStore.session_working(props.session.id)
})
const tint = createMemo(() => messageAgentColor(sessionStore.message[props.session.id], sessionStore.agent))

View File

@@ -56,6 +56,7 @@ const ProjectTile = (props: {
sidebarHovering: Accessor<boolean>
selected: Accessor<boolean>
active: Accessor<boolean>
isWorking: Accessor<boolean>
overlay: Accessor<boolean>
suppressHover: Accessor<boolean>
dirs: Accessor<string[]>
@@ -143,7 +144,7 @@ const ProjectTile = (props: {
}}
onBlur={() => props.setOpen(false)}
>
<ProjectIcon project={props.project} notify />
<ProjectIcon project={props.project} notify working={props.isWorking()} />
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content>
@@ -301,6 +302,12 @@ export const SortableProject = (props: {
}
const projectStore = createMemo(() => globalSync.child(props.project.worktree, { bootstrap: false })[0])
const isWorking = createMemo(() =>
dirs().some((directory) => {
const [store] = globalSync.child(directory, { bootstrap: false })
return Object.keys(store.session_status).some((id) => store.session_working(id))
}),
)
const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow()))
const workspaceSessions = (directory: string) => {
const [data] = globalSync.child(directory, { bootstrap: false })
@@ -313,6 +320,7 @@ export const SortableProject = (props: {
sidebarHovering={props.ctx.sidebarHovering}
selected={selected}
active={active}
isWorking={isWorking}
overlay={overlay}
suppressHover={() => state.suppressHover}
dirs={dirs}

View File

@@ -14,7 +14,7 @@ import { Spinner } from "@opencode-ai/ui/spinner"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { type Session } from "@opencode-ai/sdk/v2/client"
import { type LocalProject } from "@/context/layout"
import { loadSessionsQueryKey, useGlobalSync } from "@/context/global-sync"
import { useGlobalSync, useQueryOptions } from "@/context/global-sync"
import { useLanguage } from "@/context/language"
import { pathKey } from "@/utils/path-key"
import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
@@ -300,6 +300,7 @@ export const SortableWorkspace = (props: {
const navigate = useNavigate()
const params = useParams()
const globalSync = useGlobalSync()
const queryOptions = useQueryOptions()
const language = useLanguage()
const sortable = createSortable(props.directory)
const [workspaceStore, setWorkspaceStore] = globalSync.child(props.directory, { bootstrap: false })
@@ -320,7 +321,7 @@ export const SortableWorkspace = (props: {
const boot = createMemo(() => open() || active())
const count = createMemo(() => sessions()?.length ?? 0)
const hasMore = createMemo(() => workspaceStore.sessionTotal > count())
const fetching = useIsFetching(() => ({ queryKey: loadSessionsQueryKey(props.directory) }))
const fetching = useIsFetching(() => queryOptions.sessions(pathKey(props.directory)))
const busy = createMemo(() => props.ctx.isBusy(props.directory))
const loading = () => fetching() > 0 && count() === 0
const touch = createMediaQuery("(hover: none)")
@@ -446,6 +447,7 @@ export const LocalWorkspace = (props: {
mobile?: boolean
}): JSX.Element => {
const globalSync = useGlobalSync()
const queryOptions = useQueryOptions()
const language = useLanguage()
const workspace = createMemo(() => {
const [store, setStore] = globalSync.child(props.project.worktree)
@@ -454,7 +456,7 @@ export const LocalWorkspace = (props: {
const slug = createMemo(() => base64Encode(props.project.worktree))
const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow()))
const count = createMemo(() => sessions()?.length ?? 0)
const fetching = useIsFetching(() => ({ queryKey: loadSessionsQueryKey(props.project.worktree) }))
const fetching = useIsFetching(() => queryOptions.sessions(pathKey(props.project.worktree)))
const hasMore = createMemo(() => workspace().store.sessionTotal > count())
const loading = () => fetching() > 0 && count() === 0
const loadMore = async () => {

View File

@@ -1496,12 +1496,7 @@ export default function Page() {
return out
})
const busy = (sessionID: string) => {
if ((sync.data.session_status[sessionID] ?? { type: "idle" as const }).type !== "idle") return true
return (sync.data.message[sessionID] ?? []).some(
(item) => item.role === "assistant" && typeof item.time.completed !== "number",
)
}
const busy = (sessionID: string) => sync.data.session_working(sessionID)
const queuedFollowups = createMemo(() => {
const id = params.id

View File

@@ -2,6 +2,7 @@ import { Show, createEffect, createMemo, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { useNavigate } from "@solidjs/router"
import { useSpring } from "@opencode-ai/ui/motion-spring"
import { useLayout } from "@/context/layout"
import { PromptInput } from "@/components/prompt-input"
import { useLanguage } from "@/context/language"
import { usePrompt } from "@/context/prompt"
@@ -46,10 +47,12 @@ export function SessionComposerRegion(props: {
setPromptDockRef: (el: HTMLDivElement) => void
}) {
const navigate = useNavigate()
const layout = useLayout()
const prompt = usePrompt()
const language = useLanguage()
const route = useSessionKey()
const sync = useSync()
const view = layout.view(route.sessionKey)
const handoffPrompt = createMemo(() => getSessionHandoff(route.sessionKey())?.prompt)
const info = createMemo(() => (route.params.id ? sync.session.get(route.params.id) : undefined))
@@ -207,6 +210,8 @@ export function SessionComposerRegion(props: {
<SessionTodoDock
sessionID={route.params.id}
todos={props.state.todos()}
collapsed={view.todoCollapsed.get()}
onToggle={() => view.todoCollapsed.set(!view.todoCollapsed.get())}
collapseLabel={language.t("session.todo.collapse")}
expandLabel={language.t("session.todo.expand")}
dockProgress={value()}

View File

@@ -57,14 +57,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
() => todos().length > 0 && todos().every((todo) => todo.status === "completed" || todo.status === "cancelled"),
)
const status = createMemo(() => {
const id = params.id
if (!id) return idle
return sync.data.session_status[id] ?? idle
})
const busy = createMemo(() => status().type !== "idle")
const live = createMemo(() => busy() || blocked())
const live = createMemo(() => sync.data.session_working(params.id ?? "") || blocked())
const [store, setStore] = createStore({
responding: undefined as string | undefined,

View File

@@ -42,18 +42,17 @@ function dot(status: Todo["status"]) {
export function SessionTodoDock(props: {
sessionID?: string
todos: Todo[]
collapsed: boolean
onToggle: () => void
collapseLabel: string
expandLabel: string
dockProgress: number
}) {
const language = useLanguage()
const [store, setStore] = createStore({
collapsed: false,
height: 320,
})
const toggle = () => setStore("collapsed", (value) => !value)
const total = createMemo(() => props.todos.length)
const done = createMemo(() => props.todos.filter((todo) => todo.status === "completed").length)
const label = createMemo(() => language.t("session.todo.progress", { done: done(), total: total() }))
@@ -72,7 +71,7 @@ export function SessionTodoDock(props: {
)
const preview = createMemo(() => active()?.content ?? "")
const collapse = useSpring(() => (store.collapsed ? 1 : 0), { visualDuration: 0.3, bounce: 0 })
const collapse = useSpring(() => (props.collapsed ? 1 : 0), { visualDuration: 0.3, bounce: 0 })
const dock = createMemo(() => Math.max(0, Math.min(1, props.dockProgress)))
const shut = createMemo(() => 1 - dock())
const value = createMemo(() => Math.max(0, Math.min(1, collapse())))
@@ -107,11 +106,11 @@ export function SessionTodoDock(props: {
class="pl-3 pr-2 py-2 flex items-center gap-2 overflow-visible"
role="button"
tabIndex={0}
onClick={toggle}
onClick={props.onToggle}
onKeyDown={(event) => {
if (event.key !== "Enter" && event.key !== " ") return
event.preventDefault()
toggle()
props.onToggle()
}}
>
<span
@@ -148,7 +147,7 @@ export function SessionTodoDock(props: {
>
<TextReveal
class="text-14-regular text-text-base cursor-default"
text={store.collapsed ? preview() : undefined}
text={props.collapsed ? preview() : undefined}
duration={600}
travel={25}
edge={17}
@@ -161,7 +160,7 @@ export function SessionTodoDock(props: {
<div class="ml-auto">
<IconButton
data-action="session-todo-toggle-button"
data-collapsed={store.collapsed ? "true" : "false"}
data-collapsed={props.collapsed ? "true" : "false"}
icon="chevron-down"
size="normal"
variant="ghost"
@@ -172,16 +171,16 @@ export function SessionTodoDock(props: {
}}
onClick={(event) => {
event.stopPropagation()
toggle()
props.onToggle()
}}
aria-label={store.collapsed ? props.expandLabel : props.collapseLabel}
aria-label={props.collapsed ? props.expandLabel : props.collapseLabel}
/>
</div>
</div>
<div
data-slot="session-todo-list"
aria-hidden={store.collapsed || off()}
aria-hidden={props.collapsed || off()}
classList={{
"pointer-events-none": hide() > 0.1,
}}

View File

@@ -32,7 +32,7 @@ export interface SessionReviewTabProps {
focusedComment?: { file: string; id: string } | null
onFocusedCommentChange?: (focus: { file: string; id: string } | null) => void
focusedFile?: string
onScrollRef?: (el: HTMLDivElement) => void
onScrollRef?: (el: HTMLDivElement | undefined) => void
commentMentions?: {
items: (query: string) => string[] | Promise<string[]>
}
@@ -126,6 +126,7 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
onCleanup(() => {
if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame)
props.onScrollRef?.(undefined)
})
return (

View File

@@ -28,6 +28,12 @@ import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type S
import { setSessionHandoff } from "@/pages/session/handoff"
import { useSessionLayout } from "@/pages/session/session-layout"
type RenderDiff = (SnapshotFileDiff & { file: string }) | VcsFileDiff
function renderDiff(value: SnapshotFileDiff | VcsFileDiff): value is RenderDiff {
return typeof value.file === "string"
}
export function SessionSidePanel(props: {
canReview: () => boolean
diffs: () => (SnapshotFileDiff | VcsFileDiff)[]
@@ -70,7 +76,8 @@ export function SessionSidePanel(props: {
})
const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px"))
const diffFiles = createMemo(() => props.diffs().map((d) => d.file))
const diffs = createMemo(() => props.diffs().filter(renderDiff))
const diffFiles = createMemo(() => diffs().map((d) => d.file))
const kinds = createMemo(() => {
const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
if (!a) return b
@@ -81,7 +88,7 @@ export function SessionSidePanel(props: {
const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "")
const out = new Map<string, "add" | "del" | "mix">()
for (const diff of props.diffs()) {
for (const diff of diffs()) {
const file = normalize(diff.file)
const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix"
@@ -214,239 +221,241 @@ export function SessionSidePanel(props: {
}}
style={{ width: panelWidth() }}
>
<div class="size-full flex border-l border-border-weaker-base">
<div
aria-hidden={!reviewOpen()}
inert={!reviewOpen()}
class="relative min-w-0 h-full flex-1 overflow-hidden bg-background-base"
classList={{
"pointer-events-none": !reviewOpen(),
}}
>
<div class="size-full min-w-0 h-full bg-background-base">
<DragDropProvider
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
collisionDetector={closestCenter}
>
<DragDropSensors />
<ConstrainDragYAxis />
<Tabs value={activeTab()} onChange={openTab}>
<div class="sticky top-0 shrink-0 flex">
<Tabs.List
ref={(el: HTMLDivElement) => {
const stop = createFileTabListSync({ el, contextOpen })
onCleanup(stop)
}}
>
<Show when={reviewTab() && props.canReview()}>
<Tabs.Trigger value="review">
<div class="flex items-center gap-1.5">
<div>{language.t("session.tab.review")}</div>
<Show when={props.hasReview()}>
<div>{props.reviewCount()}</div>
</Show>
</div>
</Tabs.Trigger>
</Show>
<Show when={contextOpen()}>
<Tabs.Trigger
value="context"
closeButton={
<TooltipKeybind
title={language.t("common.closeTab")}
keybind={command.keybind("tab.close")}
placement="bottom"
gutter={10}
>
<IconButton
icon="close-small"
variant="ghost"
class="h-5 w-5"
onClick={() => tabs().close("context")}
aria-label={language.t("common.closeTab")}
/>
</TooltipKeybind>
}
hideCloseButton
onMiddleClick={() => tabs().close("context")}
>
<div class="flex items-center gap-2">
<SessionContextUsage variant="indicator" />
<div>{language.t("session.tab.context")}</div>
</div>
</Tabs.Trigger>
</Show>
<SortableProvider ids={openedTabs()}>
<For each={openedTabs()}>{(tab) => <SortableTab tab={tab} onTabClose={tabs().close} />}</For>
</SortableProvider>
<div class="bg-background-stronger h-full shrink-0 sticky right-0 z-10 flex items-center justify-center pr-3">
<TooltipKeybind
title={language.t("command.file.open")}
keybind={command.keybind("file.open")}
class="flex items-center"
>
<IconButton
icon="plus-small"
variant="ghost"
iconSize="large"
class="!rounded-md"
onClick={() => {
void import("@/components/dialog-select-file").then((x) => {
dialog.show(() => <x.DialogSelectFile mode="files" onOpenFile={showAllFiles} />)
})
}}
aria-label={language.t("command.file.open")}
/>
</TooltipKeybind>
</div>
</Tabs.List>
</div>
<Show when={reviewTab() && props.canReview()}>
<Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
<Show when={activeTab() === "review"}>{props.reviewPanel()}</Show>
</Tabs.Content>
</Show>
<Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
<Show when={activeTab() === "empty"}>
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
<div class="h-full px-6 pb-42 -mt-4 flex flex-col items-center justify-center text-center gap-6">
<Mark class="w-14 opacity-10" />
<div class="text-14-regular text-text-weak max-w-56">
{language.t("session.files.selectToOpen")}
</div>
</div>
</div>
</Show>
</Tabs.Content>
<Show when={contextOpen()}>
<Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
<Show when={activeTab() === "context"}>
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
<SessionContextTab />
</div>
</Show>
</Tabs.Content>
</Show>
<Show when={activeFileTab()} keyed>
{(tab) => <FileTabContent tab={tab} />}
</Show>
</Tabs>
<DragOverlay>
<Show when={store.activeDraggable} keyed>
{(tab) => {
const path = file.pathFromTab(tab)
return (
<div data-component="tabs-drag-preview">
<Show when={path}>{(p) => <FileVisual active path={p()} />}</Show>
</div>
)
}}
</Show>
</DragOverlay>
</DragDropProvider>
</div>
</div>
<Show when={shown()}>
<Show when={open()}>
<div class="size-full flex border-l border-border-weaker-base">
<div
id="file-tree-panel"
aria-hidden={!fileOpen()}
inert={!fileOpen()}
class="relative min-w-0 h-full shrink-0 overflow-hidden"
aria-hidden={!reviewOpen()}
inert={!reviewOpen()}
class="relative min-w-0 h-full flex-1 overflow-hidden bg-background-base"
classList={{
"pointer-events-none": !fileOpen(),
"transition-[width] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
!props.size.active(),
"pointer-events-none": !reviewOpen(),
}}
style={{ width: treeWidth() }}
>
<div
class="h-full flex flex-col overflow-hidden group/filetree"
classList={{ "border-l border-border-weaker-base": reviewOpen() }}
>
<Tabs
variant="pill"
value={fileTreeTab()}
onChange={setFileTreeTabValue}
class="h-full"
data-scope="filetree"
<div class="size-full min-w-0 h-full bg-background-base">
<DragDropProvider
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
collisionDetector={closestCenter}
>
<Tabs.List>
<Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
{props.reviewCount()}{" "}
{language.t(
props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other",
)}
</Tabs.Trigger>
<Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
{language.t("session.files.all")}
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
<Switch>
<Match when={props.hasReview() || !props.diffsReady()}>
<Show
when={props.diffsReady()}
fallback={
<div class="px-2 py-2 text-12-regular text-text-weak">
{language.t("common.loading")}
{language.t("common.loading.ellipsis")}
<DragDropSensors />
<ConstrainDragYAxis />
<Tabs value={activeTab()} onChange={openTab}>
<div class="sticky top-0 shrink-0 flex">
<Tabs.List
ref={(el: HTMLDivElement) => {
const stop = createFileTabListSync({ el, contextOpen })
onCleanup(stop)
}}
>
<Show when={reviewTab() && props.canReview()}>
<Tabs.Trigger value="review">
<div class="flex items-center gap-1.5">
<div>{language.t("session.tab.review")}</div>
<Show when={props.hasReview()}>
<div>{props.reviewCount()}</div>
</Show>
</div>
}
>
</Tabs.Trigger>
</Show>
<Show when={contextOpen()}>
<Tabs.Trigger
value="context"
closeButton={
<TooltipKeybind
title={language.t("common.closeTab")}
keybind={command.keybind("tab.close")}
placement="bottom"
gutter={10}
>
<IconButton
icon="close-small"
variant="ghost"
class="h-5 w-5"
onClick={() => tabs().close("context")}
aria-label={language.t("common.closeTab")}
/>
</TooltipKeybind>
}
hideCloseButton
onMiddleClick={() => tabs().close("context")}
>
<div class="flex items-center gap-2">
<SessionContextUsage variant="indicator" />
<div>{language.t("session.tab.context")}</div>
</div>
</Tabs.Trigger>
</Show>
<SortableProvider ids={openedTabs()}>
<For each={openedTabs()}>{(tab) => <SortableTab tab={tab} onTabClose={tabs().close} />}</For>
</SortableProvider>
<div class="bg-background-stronger h-full shrink-0 sticky right-0 z-10 flex items-center justify-center pr-3">
<TooltipKeybind
title={language.t("command.file.open")}
keybind={command.keybind("file.open")}
class="flex items-center"
>
<IconButton
icon="plus-small"
variant="ghost"
iconSize="large"
class="!rounded-md"
onClick={() => {
void import("@/components/dialog-select-file").then((x) => {
dialog.show(() => <x.DialogSelectFile mode="files" onOpenFile={showAllFiles} />)
})
}}
aria-label={language.t("command.file.open")}
/>
</TooltipKeybind>
</div>
</Tabs.List>
</div>
<Show when={reviewTab() && props.canReview()}>
<Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
<Show when={reviewOpen() && activeTab() === "review"}>{props.reviewPanel()}</Show>
</Tabs.Content>
</Show>
<Tabs.Content value="empty" class="flex flex-col h-full overflow-hidden contain-strict">
<Show when={activeTab() === "empty"}>
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
<div class="h-full px-6 pb-42 -mt-4 flex flex-col items-center justify-center text-center gap-6">
<Mark class="w-14 opacity-10" />
<div class="text-14-regular text-text-weak max-w-56">
{language.t("session.files.selectToOpen")}
</div>
</div>
</div>
</Show>
</Tabs.Content>
<Show when={contextOpen()}>
<Tabs.Content value="context" class="flex flex-col h-full overflow-hidden contain-strict">
<Show when={activeTab() === "context"}>
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
<SessionContextTab />
</div>
</Show>
</Tabs.Content>
</Show>
<Show when={activeFileTab()} keyed>
{(tab) => <FileTabContent tab={tab} />}
</Show>
</Tabs>
<DragOverlay>
<Show when={store.activeDraggable} keyed>
{(tab) => {
const path = file.pathFromTab(tab)
return (
<div data-component="tabs-drag-preview">
<Show when={path}>{(p) => <FileVisual active path={p()} />}</Show>
</div>
)
}}
</Show>
</DragOverlay>
</DragDropProvider>
</div>
</div>
<Show when={shown()}>
<div
id="file-tree-panel"
aria-hidden={!fileOpen()}
inert={!fileOpen()}
class="relative min-w-0 h-full shrink-0 overflow-hidden"
classList={{
"pointer-events-none": !fileOpen(),
"transition-[width] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
!props.size.active(),
}}
style={{ width: treeWidth() }}
>
<div
class="h-full flex flex-col overflow-hidden group/filetree"
classList={{ "border-l border-border-weaker-base": reviewOpen() }}
>
<Tabs
variant="pill"
value={fileTreeTab()}
onChange={setFileTreeTabValue}
class="h-full"
data-scope="filetree"
>
<Tabs.List>
<Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
{props.reviewCount()}{" "}
{language.t(
props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other",
)}
</Tabs.Trigger>
<Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
{language.t("session.files.all")}
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
<Switch>
<Match when={props.hasReview() || !props.diffsReady()}>
<Show
when={props.diffsReady()}
fallback={
<div class="px-2 py-2 text-12-regular text-text-weak">
{language.t("common.loading")}
{language.t("common.loading.ellipsis")}
</div>
}
>
<FileTree
path=""
class="pt-3"
allowed={diffFiles()}
kinds={kinds()}
draggable={false}
active={props.activeDiff}
onFileClick={(node) => props.focusReviewDiff(node.path)}
/>
</Show>
</Match>
</Switch>
</Tabs.Content>
<Tabs.Content value="all" class="bg-background-stronger px-3 py-0">
<Switch>
<Match when={nofiles()}>{empty(language.t("session.files.empty"))}</Match>
<Match when={true}>
<FileTree
path=""
class="pt-3"
allowed={diffFiles()}
modified={diffFiles()}
kinds={kinds()}
draggable={false}
active={props.activeDiff}
onFileClick={(node) => props.focusReviewDiff(node.path)}
onFileClick={(node) => openTab(file.tab(node.path))}
/>
</Show>
</Match>
</Switch>
</Tabs.Content>
<Tabs.Content value="all" class="bg-background-stronger px-3 py-0">
<Switch>
<Match when={nofiles()}>{empty(language.t("session.files.empty"))}</Match>
<Match when={true}>
<FileTree
path=""
class="pt-3"
modified={diffFiles()}
kinds={kinds()}
onFileClick={(node) => openTab(file.tab(node.path))}
/>
</Match>
</Switch>
</Tabs.Content>
</Tabs>
</div>
<Show when={fileOpen()}>
<div onPointerDown={() => props.size.start()}>
<ResizeHandle
direction="horizontal"
edge="start"
size={layout.fileTree.width()}
min={200}
max={480}
onResize={(width) => {
props.size.touch()
layout.fileTree.resize(width)
}}
/>
</Match>
</Switch>
</Tabs.Content>
</Tabs>
</div>
</Show>
</div>
</Show>
</div>
<Show when={fileOpen()}>
<div onPointerDown={() => props.size.start()}>
<ResizeHandle
direction="horizontal"
edge="start"
size={layout.fileTree.width()}
min={200}
max={480}
onResize={(width) => {
props.size.touch()
layout.fileTree.resize(width)
}}
/>
</div>
</Show>
</div>
</Show>
</div>
</Show>
</aside>
</Show>
)

View File

@@ -75,8 +75,6 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" ||
settings.general.showFileTree()
const idle = { type: "idle" as const }
const status = () => sync.data.session_status[params.id ?? ""] ?? idle
const messages = () => {
const id = params.id
if (!id) return []
@@ -290,7 +288,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
const sessionID = params.id
if (!sessionID) return
if (status().type !== "idle") {
if (sync.data.session_working(params.id ?? "")) {
await sdk.client.session.abort({ sessionID }).catch(() => {})
}

View File

@@ -1,5 +1,3 @@
import z from "zod"
const prefixes = {
session: "ses",
message: "msg",
@@ -15,10 +13,6 @@ let counter = 0
type Prefix = keyof typeof prefixes
export namespace Identifier {
export function schema(prefix: Prefix) {
return z.string().startsWith(prefixes[prefix])
}
export function ascending(prefix: Prefix, given?: string) {
return generateID(prefix, false, given)
}

View File

@@ -128,4 +128,17 @@ describe("formatServerError", () => {
["Modelo nao encontrado: x/y", "Voce quis dizer: x/y2, x/y3", "Revise provider/model no config"].join("\n"),
)
})
test("unwraps SDK-wrapped errors from cause.body", () => {
const body = {
name: "ConfigInvalidError",
data: {
message: "Missing host",
},
} satisfies ConfigInvalidError
const wrapped = new Error("ConfigInvalidError", { cause: { body, status: 400 } })
expect(formatServerError(wrapped, language.t)).toBe("Arquivo de config em config invalido: Missing host")
})
})

View File

@@ -26,14 +26,22 @@ function tr(translator: Translator | undefined, key: string, text: string, vars?
}
export function formatServerError(error: unknown, translate?: Translator, fallback?: string) {
if (isConfigInvalidErrorLike(error)) return parseReadableConfigInvalidError(error, translate)
if (isProviderModelNotFoundErrorLike(error)) return parseReadableProviderModelNotFoundError(error, translate)
const unwrapped = unwrapNamedError(error)
if (isConfigInvalidErrorLike(unwrapped)) return parseReadableConfigInvalidError(unwrapped, translate)
if (isProviderModelNotFoundErrorLike(unwrapped)) return parseReadableProviderModelNotFoundError(unwrapped, translate)
if (error instanceof Error && error.message) return error.message
if (typeof error === "string" && error) return error
if (fallback) return fallback
return tr(translate, "error.chain.unknown", "Unknown error")
}
function unwrapNamedError(error: unknown): unknown {
if (error instanceof Error && error.cause && typeof error.cause === "object" && "body" in error.cause) {
return (error.cause as Record<string, unknown>).body
}
return error
}
function isConfigInvalidErrorLike(error: unknown): error is ConfigInvalidError {
if (typeof error !== "object" || error === null) return false
const o = error as Record<string, unknown>

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.14.39",
"version": "1.14.51",
"type": "module",
"license": "MIT",
"scripts": {
@@ -31,11 +31,11 @@
"solid-js": "catalog:",
"solid-list": "0.3.0",
"solid-stripe": "0.8.1",
"svix": "1.92.2",
"vite": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
"@webgpu/types": "0.1.54",
"typescript": "catalog:",

View File

@@ -9,8 +9,8 @@ export const config = {
github: {
repoUrl: "https://github.com/anomalyco/opencode",
starsFormatted: {
compact: "150K",
full: "150,000",
compact: "160K",
full: "160,000",
},
},
@@ -22,8 +22,8 @@ export const config = {
// Static stats (used on landing page)
stats: {
contributors: "850",
commits: "11,000",
monthlyUsers: "6.5M",
contributors: "900",
commits: "13,000",
monthlyUsers: "7.5M",
},
} as const

View File

@@ -355,8 +355,12 @@ export const dict = {
"zen.api.error.missingApiKey": "مفتاح API مفقود.",
"zen.api.error.invalidApiKey": "مفتاح API غير صالح.",
"zen.api.error.subscriptionQuotaExceeded": "تم تجاوز حصة الاشتراك. أعد المحاولة خلال {{retryIn}}.",
"zen.api.error.subscriptionQuotaExceededUseFreeModels":
"تم تجاوز حصة الاشتراك. يمكنك الاستمرار في استخدام النماذج المجانية.",
"zen.api.error.goSubscriptionRollingLimitExceeded":
"تم الوصول إلى حد الاستخدام لمدة 5 ساعات. تتم إعادة التعيين خلال {{retryIn}}. لمواصلة استخدام هذا النموذج الآن، فعّل الاستخدام من رصيدك المتاح: {{consoleGoUrl}}",
"zen.api.error.goSubscriptionWeeklyLimitExceeded":
"تم الوصول إلى حد الاستخدام الأسبوعي. تتم إعادة التعيين خلال {{retryIn}}. لمواصلة استخدام هذا النموذج الآن، فعّل الاستخدام من رصيدك المتاح: {{consoleGoUrl}}",
"zen.api.error.goSubscriptionMonthlyLimitExceeded":
"تم الوصول إلى حد الاستخدام الشهري. تتم إعادة التعيين خلال {{retryIn}}. لمواصلة استخدام هذا النموذج الآن، فعّل الاستخدام من رصيدك المتاح: {{consoleGoUrl}}",
"zen.api.error.noPaymentMethod": "لا توجد طريقة دفع. أضف طريقة دفع هنا: {{billingUrl}}",
"zen.api.error.insufficientBalance": "رصيد غير كاف. إدارة فواتيرك هنا: {{billingUrl}}",
"zen.api.error.workspaceMonthlyLimitReached":

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