Compare commits

...
Author SHA1 Message Date
opencode-agent[bot] 2d15d89140 chore: update nix node_modules hashes
deploy / deploy (push) Has been cancelled
generate / generate (push) Has been cancelled
nix-eval / nix-eval (push) Has been cancelled
publish / version (push) Has been cancelled
typecheck / typecheck (push) Has been cancelled
publish / build-cli (push) Has been cancelled
publish / sign-cli-windows (push) Has been cancelled
publish / build-electron (map[bun_install_flags:--os=darwin --cpu=arm64 host:macos-26 platform_flag:--mac --arm64 target:aarch64-apple-darwin]) (push) Has been cancelled
publish / build-electron (map[bun_install_flags:--os=darwin --cpu=x64 host:macos-26-intel platform_flag:--mac --x64 target:x86_64-apple-darwin]) (push) Has been cancelled
publish / build-electron (map[host:blacksmith-4vcpu-ubuntu-2404 platform_flag:--linux target:x86_64-unknown-linux-gnu]) (push) Has been cancelled
publish / build-electron (map[host:blacksmith-4vcpu-ubuntu-2404-arm platform_flag:--linux --arm64 target:aarch64-unknown-linux-gnu]) (push) Has been cancelled
publish / build-electron (map[host:blacksmith-4vcpu-windows-2025 platform_flag:--win target:x86_64-pc-windows-msvc]) (push) Has been cancelled
publish / build-electron (map[host:windows-2025 platform_flag:--win --arm64 target:aarch64-pc-windows-msvc]) (push) Has been cancelled
publish / publish (push) Has been cancelled
2026-05-28 18:05:19 +00:00
Frank 2eb0606998 zen: add opus 4.8
deploy / deploy (push) Has been cancelled
2026-05-28 13:48:05 -04:00
opencode-agent[bot] cc230feca1 chore: generate 2026-05-28 17:45:17 +00:00
Adam 3ce9b4be0d fix(stats): refine hero layout for responsive Figma match 2026-05-28 12:43:16 -05:00
Adam fff7781fa8 fix(stats): preload IBM Plex Mono weights 2026-05-28 12:43:15 -05:00
Adam 0a72298062 feat(stats): better hero 2026-05-28 12:43:15 -05:00
Adam 7c320fd463 feat(stats): better header 2026-05-28 12:43:15 -05:00
Aiden Cline 05e3c4ecee fix(opencode): enable adaptive reasoning for opus 4.7+ (#29769) 2026-05-28 12:19:34 -05:00
FilipandKit Langton 72d008bd5c fix(core): allow colons inside passwords (#29576)
Co-authored-by: Kit Langton <kit.langton@gmail.com>
2026-05-28 13:17:35 -04:00
202ec12056 docs: fix grammar in config.mdx (#29683)
Co-authored-by: dnsnav <dnsnav@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-28 11:25:34 -05:00
Aiden Cline af30b315b2 fix(tui): restore thinking spinner (#29765) 2026-05-28 11:24:45 -05:00
opencode-agent[bot] 425a4f584f chore: generate 2026-05-28 13:39:37 +00:00
Adam 7504daa602 fix(stats): batch honeycomb backfill 2026-05-28 08:37:49 -05:00
Adam 9ac0f3e9ac fix(stats): better stat table periods, provider/model norm 2026-05-28 08:37:48 -05:00
Adam cf2cd13fb8 chore: updated backfill 2026-05-28 08:37:48 -05:00
opencode-agent[bot] 6883309997 chore: generate 2026-05-28 13:24:10 +00:00
Shoubhit Dash c3562a4a51 test(acp): remove timing diagnostics (#29741) 2026-05-28 18:52:37 +05:30
opencode-agent[bot] 30a66dfc31 chore: generate 2026-05-28 11:51:48 +00:00
Victor Navarro fdc574ff81 feat: improve referral system (#29720) 2026-05-28 13:50:14 +02:00
opencode-agent[bot] 1e5ddbd812 chore: generate 2026-05-28 10:56:37 +00:00
Shoubhit Dash 3109060216 fix(acp): cover smoke parity gaps (#29719) 2026-05-28 16:25:00 +05:30
Shoubhit Dash 9031ce7b51 fix(acp): speed up acp-next warm switches (#29713) 2026-05-28 15:10:17 +05:30
Shoubhit Dash 2449b50585 fix(acp): improve acp-next first-session startup (#29709) 2026-05-28 14:53:47 +05:30
opencode-agent[bot] 66e6662440 chore: generate 2026-05-28 08:48:09 +00:00
Shoubhit Dash 56f3b9044d test(acp-next): add local verifier subprocess tests (#29700) 2026-05-28 14:16:49 +05:30
Luke Parker 913659890d fix(openai): keep websocket response timeouts active (#29699) 2026-05-28 18:28:01 +10:00
Aiden Cline 14e0b9b17f fix(openai): retry websocket stream failures (#29673) 2026-05-28 01:16:18 -05:00
opencode-agent[bot] e5524f5bf9 chore: generate 2026-05-28 05:15:21 +00:00
Shoubhit Dash 16234a0baf feat(acp): implement acp-next prompt (#29664) 2026-05-28 10:43:59 +05:30
Frank e5cf3ad3c2 zen: fix db issue 2026-05-28 00:50:15 -04:00
opencode-agent[bot] 82da3320ba chore: generate 2026-05-28 03:43:10 +00:00
Shoubhit Dash aa553dea94 fix(acp): handle acp-next permission events (#29656) 2026-05-28 09:11:49 +05:30
opencode-agent[bot] 0c9cad86ec chore: generate 2026-05-28 02:59:53 +00:00
James Long 69910f361c fix(server): use persisted session directory for existing-session routes (#29640) 2026-05-27 22:58:33 -04:00
Kit Langton 7bafbb309a fix(opencode): forward remote workspace request bodies (#29458) 2026-05-27 21:54:36 -04:00
DS ec26d78450 fix(openai): support websocket custom base URLs (#29636) 2026-05-27 18:46:30 -05:00
opencode-agent[bot] 49e8d324dd chore: update nix node_modules hashes 2026-05-27 22:16:44 +00:00
Sebastian 60b9ba4db7 upgrade opentui to 0.2.16 (#29463) 2026-05-28 00:01:32 +02:00
opencode-agent[bot] a1a401dc9b chore: update nix node_modules hashes 2026-05-27 20:22:29 +00:00
opencode-agent[bot] 2f1547ebaf chore: generate 2026-05-27 20:05:11 +00:00
Aiden Cline 62da1e7682 feat(openai): add responses websocket transport (#29477) 2026-05-27 15:02:37 -05:00
James Long 28a06e52fc feat(tui): add workspace management dialog (#29612) 2026-05-27 18:09:40 +00:00
Jack 5a5d981c4e docs(zen): add MiMo-V2.5 Free model (#29610) 2026-05-28 01:34:37 +08:00
opencode-agent[bot] 62a7781e04 chore: generate 2026-05-27 16:31:18 +00:00
Jack 2d03cf4a9a docs(go): update MiMo (#29598) 2026-05-28 00:29:31 +08:00
opencode-agent[bot] 4025d30b24 chore: generate 2026-05-27 15:53:52 +00:00
Adam dd3c443d8c fix: cpu bound ingest server 2026-05-27 10:51:36 -05:00
Adam 4aaa9f9e30 chore: better stats command 2026-05-27 10:50:34 -05:00
Adam 31c805bc97 chore: honeycomb backfill script 2026-05-27 10:37:02 -05:00
Sebastian 94f2ed1b84 keep session navigation active in prompt modes (#29464) 2026-05-27 17:26:56 +02:00
gotjoshua 86dc66eae9 docs: fix ts error in math examples (#18244) 2026-05-27 10:12:12 -05:00
Aiden Cline 9814dc6526 fix(tui): surface subagent retry status (#29591) 2026-05-27 10:11:01 -05:00
Adam f09c859974 fix: memory pressure in ingest 2026-05-27 09:22:08 -05:00
Adam 1fcdb0246a fix: bun install 2026-05-27 08:37:34 -05:00
opencode-agent[bot] 340ad9e9ed chore: generate 2026-05-27 12:47:27 +00:00
Kit Langton 76d814e747 refactor(server): unify instance httpapi middleware routing
publish-github-action / publish (push) Has been cancelled
Unify declared instance HTTP API endpoints under typed middleware routing, including event streaming and PTY WebSocket connect handling.\n\nPreserve PTY connect compatibility by checking missing PTYs before parsing optional cursor and ticket query fields, with regression coverage.
2026-05-27 08:45:11 -04:00
Adam 9941e70d29 fix: better dockerfile 2026-05-27 06:55:44 -05:00
Adam 2afb812620 fix: stats dockerfile
publish-github-action / publish (push) Has been cancelled
2026-05-27 06:39:39 -05:00
Adam ad2fc1ce4b chore: reestablish datalake 2026-05-27 06:17:16 -05:00
Victor Navarro 5acc368ef4 perf: use redis for api key rate limit (#29242) 2026-05-27 11:27:06 +02:00
Brendan Allan e2dc89c6f3 fix(ui): migrate to --v2-font-family-sans variable and improve home empty state (#29552) 2026-05-27 09:18:52 +00:00
Brendan Allan e1581183ff feat(app): allow toggling tabs layout (#29526) 2026-05-27 15:42:21 +08:00
Brendan Allan f195c952fc fix(app): show tab close button properly (#29523) 2026-05-27 06:36:04 +00:00
opencode-agent[bot] 34b1045a01 chore: generate 2026-05-27 04:58:46 +00:00
OpeOginni aa9956d46d fix(tui): handle non-git project paths when opening editor (#29180) 2026-05-26 23:57:24 -05:00
opencode cec5a752bb sync release versions for v1.15.11 2026-05-27 04:01:11 +00:00
opencode-agent[bot] 0f0a4be253 fix(project): restore remote project identity (#29495) 2026-05-27 03:09:20 +00:00
opencode-agent[bot] 0e88dd99e7 chore: generate 2026-05-27 02:24:30 +00:00
Aiden Cline f965db9e13 feat: add headerTimeout cfg option, default it on only for openai w/ default of 10s (#29484) 2026-05-26 21:22:24 -05:00
Aiden Cline 519d344470 feat(plugin): add dispose hook (#29493) 2026-05-26 21:21:53 -05:00
Aiden Cline a78605f8ea fix(plugin): use codex session-id header (#29471) 2026-05-26 19:16:07 -05:00
Aiden Cline c0bc020ad6 fix(opencode): disconnect dynamically added mcp servers (#29452) 2026-05-26 18:28:03 -05:00
Frank 0e669bbd18 zen: disable models 2026-05-26 18:49:10 -04:00
Dax Raad 00a6f228f2 fix(project): preserve cached project identity 2026-05-26 17:50:42 -04:00
James Long 0ba1081cf1 fix(tui): accelerate diff viewer scrolling (#29453) 2026-05-26 20:53:13 +00:00
MusaAiden ClineAiden Clinegithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
0448a30821 fix(digitalocean): use OAuth token directly for inference instead of creating MAK (#28897)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-26 14:20:25 -05:00
Sebastian 848d763d08 Prepare TUI lifecycle for scenario tests (#28258) 2026-05-26 21:02:02 +02:00
Dustin Deusandstarptech fdfd0afed7 docs: add tip to lsp section (#29444)
Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box>
2026-05-26 13:59:05 -05:00
Dustin Deusandstarptech 5f31d2ca88 docs(web): update LSP docs wording (#29435)
Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box>
2026-05-26 12:59:51 -05:00
Frank afdae3950a sync 2026-05-26 13:41:49 -04:00
opencode-agent[bot] 50b869a508 chore: update nix node_modules hashes 2026-05-26 17:40:59 +00:00
opencode-agent[bot] cf8b03ec10 chore: generate 2026-05-26 17:29:38 +00:00
Jack 1554a5a82e docs(go): add Qwen3.7 Max (#29389) 2026-05-26 13:26:57 -04:00
Aiden Cline 797359fbf0 fix(opencode): patch gcp metadata warning leak (#29431) 2026-05-26 12:18:09 -05:00
opencode-agent[bot] 034298cb1f chore: update nix node_modules hashes 2026-05-26 16:55:51 +00:00
Aiden Cline 1c4196c358 docs: remove ambiguous agent automation guidance (#29427) 2026-05-26 11:46:45 -05:00
Aiden Cline 28dbd4ab4a chore(opencode): pin google providers before tool id regression (#29423) 2026-05-26 11:34:54 -05:00
vimtor 715589fea4 fix: stuck sst deployment 2026-05-26 18:07:50 +02:00
opencode-agent[bot] 922903e410 chore: generate 2026-05-26 15:52:47 +00:00
vimtor 541a25c2a1 fix: stuck sst deployment 2026-05-26 17:48:27 +02:00
opencode-agent[bot] 1bf2b298e9 chore: update nix node_modules hashes 2026-05-26 14:23:57 +00:00
Frank 5ee18f9b75 ci: fix 2026-05-26 10:18:37 -04:00
Frank 26b1d14c16 ci: fix 2026-05-26 10:11:41 -04:00
opencode-agent[bot] 249dc0e01e chore: generate 2026-05-26 14:03:28 +00:00
Frank 8ad03547df zen: support dashboard 2026-05-26 10:01:20 -04:00
Eric Guo 1a8fd0e1dc fix(app): handle deleted session sync cache in V2 titlebar (#29328) 2026-05-26 13:23:17 +00:00
Adam 46140b0cc8 chore: better sync/ingest logging 2026-05-26 06:35:49 -05:00
opencode-agent[bot] ca354f8e8a chore: generate 2026-05-26 07:51:29 +00:00
Luke Parker 13c9d097a0 feat(app): refine desktop v2 home and session controls (#28788) 2026-05-26 07:50:07 +00:00
Luke Parker 1f66db0e4e test(app): restore timeline e2e server health mock (#29336) 2026-05-26 07:36:46 +00:00
Brendan Allan c71fe78970 fix(app): disable health check for web deployments (#29319) 2026-05-26 15:26:59 +08:00
Shoubhit Dash 717e74f3e5 feat(acp): stream acp-next tool updates (#29333) 2026-05-26 12:48:31 +05:30
Shoubhit Dash b5632ea700 fix(config): fallback when user info unavailable (#29332) 2026-05-26 12:42:12 +05:30
opencode-agent[bot] 8845a43e9e chore: generate 2026-05-26 06:48:48 +00:00
Shoubhit Dash 7e5305c765 feat(acp-next): add event routing (#29327) 2026-05-26 12:17:20 +05:30
opencode-agent[bot] 245f00a4b3 chore: generate 2026-05-26 06:17:48 +00:00
Shoubhit Dash 15c5ec6d24 feat(acp): add acp-next session lifecycle (#29320) 2026-05-26 11:45:24 +05:30
Brendan Allan 49707c2117 fix(app): deduplicate and merge server connections in resolveServerList (#29313) 2026-05-26 13:41:13 +08:00
opencode-agent[bot] dbb7872326 chore: update nix node_modules hashes 2026-05-26 05:01:16 +00:00
Aiden Cline 87e9e700cd fix(opencode): revert google sdk tool call id change (#29310) 2026-05-25 23:42:33 -05:00
Adam 4862c3e765 chore: remove gh role from infra 2026-05-25 20:35:45 -05:00
Brendan Allan b0fcba5724 feat(app): make server sdk + sync state global (#29285) 2026-05-26 08:20:57 +08:00
Adam 2b3ddf9f34 chore: cleanup 2026-05-25 18:18:34 -05:00
Adam 3c7f608475 fix: aws stages 2026-05-25 18:14:45 -05:00
opencode-agent[bot] 1ed14151a5 chore: update nix node_modules hashes 2026-05-25 22:49:12 +00:00
opencode-agent[bot] 16a30041b3 chore: generate 2026-05-25 22:35:28 +00:00
Adam 5b02ac4d33 feat: initial datalake and stats site (#28666) 2026-05-25 17:34:04 -05:00
Aiden Cline 633b5d6208 fix: allow experimental flags to override umbrella (#29273) 2026-05-25 16:42:34 -05:00
opencode-agent[bot] d595e472b3 chore: generate 2026-05-25 21:30:39 +00:00
Robert PoschenriederandRobert Poschenrieder 0bfa55b623 tweak (config): make modalities input/output fields optional so that u can specify one without both being required (#29268)
Co-authored-by: Robert Poschenrieder <Robert_Poschenrieder@epdeberw000f.fritz.box>
2026-05-25 16:29:11 -05:00
opencode-agent[bot] d96e3a5423 chore: generate 2026-05-25 21:11:17 +00:00
Aiden Cline dabf2dc013 remove the need for polling from experimental background agents (#29179) 2026-05-25 16:09:56 -05:00
Kit Langton 775321173d refactor(httpapi): describe bodyless global upgrade payload 2026-05-25 16:18:31 -04:00
Kit Langton b46cec2a7d fix(httpapi): model optional worktree payload as no content (#29246) 2026-05-25 15:57:12 -04:00
Kit Langton a99ebd1d65 fix(httpapi): model optional session payloads as no content (#29247) 2026-05-25 15:56:52 -04:00
Shoubhit Dash a5ea910eac fix(acp-next): add config switch fast paths (#29255) 2026-05-26 00:24:03 +05:30
Shoubhit Dash 56743dcf04 fix(acp): share acp-next session state (#29253) 2026-05-25 23:09:41 +05:30
opencode-agent[bot] 00ea47a502 chore: generate 2026-05-25 17:26:51 +00:00
Shoubhit Dash 0373ea9128 feat(acp): implement acp-next session slice (#29250) 2026-05-25 22:55:30 +05:30
Jack e1406e05a3 fix(console): bill google non-stream zen usage (#28829) 2026-05-25 13:07:50 -04:00
opencode-agent[bot] b5553839d0 chore: generate 2026-05-25 16:49:25 +00:00
Shoubhit Dash 249381e742 feat(acp-next): add usage service (#29249) 2026-05-25 22:17:11 +05:30
opencode-agent[bot] 7499021132 chore: generate 2026-05-25 16:27:24 +00:00
Shoubhit Dash 9dd24d7d03 feat(acp-next): add session state service (#29240) 2026-05-25 21:56:05 +05:30
Shoubhit Dash b2d76434ba feat(acp-next): add directory snapshot service (#29241) 2026-05-25 21:55:52 +05:30
opencode-agent[bot] d18eab5b85 chore: generate 2026-05-25 15:26:53 +00:00
Shoubhit Dash 7a5a997173 fix(acp-next): map typed errors to request errors (#29233) 2026-05-25 20:55:39 +05:30
Shoubhit Dash d200da121b feat(acp-next): add pure tool conversion helpers (#29232) 2026-05-25 20:55:22 +05:30
Shoubhit Dash fe482fe3dd test(acp-next): add config option helpers (#29234) 2026-05-25 20:54:57 +05:30
Shoubhit Dash 2fce3c1370 feat(acp-next): add content conversion helpers (#29231) 2026-05-25 20:53:58 +05:30
Shoubhit Dash 7060cfa59b feat(acp): add initial acp-next skeleton behind runtime flag (#29226) 2026-05-25 20:33:40 +05:30
opencode-agent[bot] 8077e8a9d1 chore: update nix node_modules hashes 2026-05-25 15:03:04 +00:00
opencode-agent[bot] d37166397c chore: generate 2026-05-25 14:50:18 +00:00
Shoubhit Dash 756c7c60bd test(acp): add compatibility baseline (#29222) 2026-05-25 20:19:06 +05:30
opencode-agent[bot] 94fb361591 chore: generate 2026-05-25 14:41:16 +00:00
Victor Navarro fdff82e6fc perf: use redis/upstash for ip rate limits (#28694) 2026-05-25 16:39:56 +02:00
opencode-agent[bot] b14e7451ab chore: generate 2026-05-25 14:25:37 +00:00
Braxton Schafer 0de5f1ff36 feat(tui): make prompt size responsive and configurable (#28255) 2026-05-25 09:24:19 -05:00
Victor Navarro 7703786498 perf: serve console from us-east-2 instead of us-east-1 (#28640) 2026-05-25 11:42:18 +02:00
André CruzandAiden Cline 748fcb7ebd fix(session): exclude orphaned interrupted tools from run-loop continuation (#26178)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-05-25 00:47:55 -05:00
James Long d5f397a2da fix(tui): open external editor in worktree cwd (#29130) 2026-05-24 22:40:40 -04:00
opencode-agent[bot] 5ca613e28e chore: update nix node_modules hashes 2026-05-25 02:28:11 +00:00
opencode-agent[bot] 9c7de47031 chore: generate 2026-05-25 02:06:42 +00:00
Luke Parker f023c63a60 fix(app): stabilize virtual session timeline interactions (#28422) 2026-05-25 12:05:27 +10:00
Brendan Allan 9495ecd536 refactor(app): extract refcount utility and clean up server sdk context (#29155) 2026-05-25 10:00:00 +08:00
Aiden Cline 03bb53c389 fix(tui): separate thinking header from markdown body (#29028) 2026-05-24 20:52:17 -05:00
Brendan Allan eab34b46d0 refactor(app): rename global context files to server context files (#28790) 2026-05-25 09:01:02 +08:00
Dax c5c9a1d435 fix(opencode): preserve session update time during project migration (#29147) 2026-05-24 19:31:19 -04:00
opencode-agent[bot] 47f333299b chore: generate 2026-05-24 18:08:39 +00:00
Aiden Cline db63eaf6ea tweak: make OPENCODE_EXPERIMENTAL_NATIVE_LLM separate from OPENCODE_EXPERIMENTAL (#29123) 2026-05-24 13:07:25 -05:00
opencode-agent[bot] 0cf99cf5f9 chore: update nix node_modules hashes 2026-05-24 02:24:16 +00:00
Aiden Cline d16bfe850a chore(opencode): bump amazon bedrock provider (#29052) 2026-05-23 21:06:00 -05:00
opencode-agent[bot] 728eae9c25 chore: generate 2026-05-24 01:08:00 +00:00
Aiden Cline eb84f461b8 fix(llm): split OpenAI reasoning summary blocks (#29000) 2026-05-23 20:06:45 -05:00
Kit Langton 0b3a1c2fdf test(opencode): simplify config effect tests (#29019) 2026-05-23 23:48:08 +00:00
opencode-agent[bot] 1ccd14b0e1 chore: update nix node_modules hashes 2026-05-23 20:51:28 +00:00
Aiden ClineandAndrew Sweet 61e7cdfbff fix(opencode): bump google vertex provider for thought signatures (#29010)
Co-authored-by: Andrew Sweet <andrew.sweet@cantab.net>
2026-05-23 15:29:43 -05:00
opencode-agent[bot] 7fe7b9f258 chore: update nix node_modules hashes 2026-05-23 16:42:22 +00:00
Aiden Clineanddpuyosa a5ba1d075d chore(opencode): bump venice ai sdk provider (#29003)
Co-authored-by: dpuyosa <dpuyosa@users.noreply.github.com>
2026-05-23 11:28:04 -05:00
Aiden ClineandNabs ba437069e6 fix(opencode): advertise configured shell timeout (#28998)
Co-authored-by: Nabs <nabil@instafork.com>
2026-05-23 10:43:39 -05:00
Frank 387c5a02c8 sync 2026-05-23 10:27:07 -04:00
Jérôme Benoit 4b1b722e98 fix(nix): re-inherit patches in desktop, ad-hoc sign .app on darwin (#27869) 2026-05-23 09:20:21 -05:00
Frank 6b03be5468 sync 2026-05-23 03:57:22 -04:00
opencode-agent[bot] 7d2c1ce6c6 chore: generate 2026-05-23 04:49:36 +00:00
Dax a9ef5a0fae feat(project): resolve remote-backed project identity (#28914) 2026-05-23 04:48:09 +00:00
Luke Parker 0cf9a5d20b fix(desktop): avoid white flash when restoring window (#28247) 2026-05-23 14:42:40 +10:00
opencode 2c4ad9f405 sync release versions for v1.15.10 2026-05-23 01:04:22 +00:00
Luke Parker 3bf054c1d9 fix(app): restore desktop prod legacy flows (#28919) 2026-05-22 20:40:47 -04:00
Aiden Cline 14c511e380 fix(llm): stabilize anthropic tool result typecheck (#28909) 2026-05-22 17:14:40 -05:00
opencode-agent[bot] 5ee0238c4a chore: update nix node_modules hashes 2026-05-22 21:33:19 +00:00
opencode 871d38cbd7 sync release versions for v1.15.9 2026-05-22 21:18:49 +00:00
opencode-agent[bot] 1a329e4e67 chore: generate 2026-05-22 19:31:59 +00:00
James Long bfb2d8dc76 fix(tui): when diff viewer closes always return to last route (#28903) 2026-05-22 15:30:31 -04:00
James Long 8f7a6c4a00 fix(tui): refine diff view keyboard shortcuts (#28896) 2026-05-22 14:40:14 -04:00
opencode-agent[bot] 1857c73565 chore: generate 2026-05-22 18:25:01 +00:00
Shoubhit Dash 5f42351159 fix(provider): type default model failures (#28881) 2026-05-22 23:53:23 +05:30
Shoubhit Dash 968aaa3cfe fix(pty): expose missing session errors (#28884) 2026-05-22 23:53:03 +05:30
opencode-agent[bot] b8266e5819 chore: generate 2026-05-22 18:22:48 +00:00
James Long ba746e36d8 fix(tui): empty states, context, and minor improvements to diff viewer (#28878) 2026-05-22 14:21:22 -04:00
opencode-agent[bot] d5068ba28e chore: generate 2026-05-22 17:50:23 +00:00
Shoubhit Dash 7265c46af6 fix(skill): type expected skill failures (#28885) 2026-05-22 23:18:52 +05:30
Shoubhit Dash 536ee857c6 fix(installation): type upgrade failures (#28883) 2026-05-22 23:18:17 +05:30
opencode-agent[bot] dda69d77e8 chore: generate 2026-05-22 17:47:18 +00:00
Shoubhit Dash 0e14404e5f fix(sync): map workspace warp not found (#28882) 2026-05-22 23:15:53 +05:30
opencode-agent[bot] 05f51bfe45 chore: generate 2026-05-22 17:44:34 +00:00
Shoubhit Dash aee552c043 fix(repository): type expected reference failures (#28880) 2026-05-22 23:13:14 +05:30
opencode-agent[bot] 4f6eaf859b chore: generate 2026-05-22 16:39:58 +00:00
Kit Langton d0cb58782f fix(llm): surface code, type, and nested fields on provider stream errors (#28757) 2026-05-22 12:37:55 -04:00
opencode-agent[bot] a3430db73a chore: generate 2026-05-22 16:37:46 +00:00
Shoubhit Dash 3e1972fd92 fix(httpapi): return project not found errors (#28856) 2026-05-22 22:06:20 +05:30
opencode-agent[bot] b368e5adbe chore: generate 2026-05-22 16:31:30 +00:00
James Long 69e4f52272 fix(tui): interaction improvements to diff viewer (#28851) 2026-05-22 12:30:10 -04:00
opencode-agent[bot] 8a55920531 chore: generate 2026-05-22 16:25:15 +00:00
Kit Langton 9db90a0b76 fix(llm): emit structured image blocks for tool-result media in Anthropic Messages (#28755) 2026-05-22 12:23:41 -04:00
Kit Langton 700d012025 fix(llm): emit structured input_image content for tool-result media in OpenAI Responses (#28754) 2026-05-22 12:23:23 -04:00
Shoubhit Dash 59e486a917 fix(tui): restore question prompt key handling (#28835) 2026-05-22 21:47:13 +05:30
Aiden Cline 8596967415 ci: "fix: exempt team members from compliance cleanup" (#28865) 2026-05-22 11:14:52 -05:00
opencode-agent[bot] 00038027c8 chore: generate 2026-05-22 15:31:46 +00:00
Shoubhit Dash 5cf597d583 fix(httpapi): return pty error bodies (#28838) 2026-05-22 21:00:20 +05:30
opencode-agent[bot] d92b8d8009 chore: generate 2026-05-22 13:52:40 +00:00
Dax Raad 854c535536 fix(tui): enable diff viewer by default 2026-05-22 09:50:30 -04:00
OpeOginni 3cf955e9ad fix(desktop): correct user code extraction when URL contains colons (#28837) 2026-05-22 21:16:33 +08:00
opencode-agent[bot] 63f3e84792 chore: generate 2026-05-22 12:17:49 +00:00
Shoubhit Dash 0beb4de3e8 fix(httpapi): return mcp server not found errors (#28817) 2026-05-22 17:46:30 +05:30
Shoubhit Dash 51da3483a9 feat(tui): copy worktree path from palette (#28823) 2026-05-22 17:28:51 +05:30
opencode-agent[bot] 060fbc9ce7 chore: generate 2026-05-22 11:42:14 +00:00
SebinandShoubhit Dash 7a769dab3a fix(mcp): include scope in clientMetadata and add callbackPort option (#28810)
Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
2026-05-22 17:10:38 +05:30
opencode-agent[bot] 2663ecd39d chore: generate 2026-05-22 11:00:29 +00:00
Shoubhit Dash 4ce247eaba fix(httpapi): return request not found errors (#28693) 2026-05-22 16:29:12 +05:30
Brendan Allan 76d9c2cd76 fix(app): debounce vcs refresh in session page (#28784) 2026-05-22 06:47:05 +00:00
Brendan Allan f6101aef8a refactor(app): consolidate sdk and sync contexts (#28782) 2026-05-22 06:40:56 +00:00
Brendan Allan f3874ec2f9 refactor(app): move tab navigation to titlebar and conditionally register project shortcuts (#28773) 2026-05-22 13:54:25 +08:00
Brendan Allan 6466fcfdea refactor(app): simplify session routing and tab close handling (#28767) 2026-05-22 12:48:09 +08:00
Brendan Allan 1f0390cfbb app: wrap provider data in Map to avoid store (#28765) 2026-05-22 12:23:16 +08:00
opencode-agent[bot] 9f06accfb4 chore: generate 2026-05-22 04:04:41 +00:00
JPFrancoiaandAiden Cline 7a9724496b fix(vertex): Vertex (Antropic) provider: use .rep.googleapis.com for continental multi-region endpoints (us, eu) (#28347)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
2026-05-21 23:03:24 -05:00
582 changed files with 39754 additions and 5864 deletions
+12
View File
@@ -0,0 +1,12 @@
.git
.opencode
.sst
.turbo
.wrangler
node_modules
**/node_modules
**/.output
**/dist
**/.turbo
**/.vite
**/coverage
-14
View File
@@ -34,25 +34,11 @@ jobs:
const now = Date.now(); const now = Date.now();
const twoHours = 2 * 60 * 60 * 1000; const twoHours = 2 * 60 * 60 * 1000;
const teamAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR'];
for (const item of items) { for (const item of items) {
const isPR = !!item.pull_request; const isPR = !!item.pull_request;
const kind = isPR ? 'PR' : 'issue'; const kind = isPR ? 'PR' : 'issue';
if (teamAssociations.includes(item.author_association)) {
core.info(`Skipping ${kind} #${item.number}: author association is ${item.author_association}`);
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: item.number,
name: 'needs:compliance',
});
} catch (e) {}
continue;
}
const { data: comments } = await github.rest.issues.listComments({ const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
+12
View File
@@ -9,9 +9,15 @@ on:
concurrency: ${{ github.workflow }}-${{ github.ref }} concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: read
id-token: write
jobs: jobs:
deploy: deploy:
if: github.repository == 'anomalyco/opencode' && (github.ref_name == 'dev' || github.ref_name == 'production')
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: ${{ github.ref_name }}
steps: steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
@@ -21,6 +27,12 @@ jobs:
with: with:
node-version: "24" node-version: "24"
- uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1
with:
role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
role-session-name: opencode-${{ github.run_id }}
aws-region: us-east-1
- run: bun sst deploy --stage=${{ github.ref_name }} - run: bun sst deploy --stage=${{ github.ref_name }}
env: env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+2 -2
View File
@@ -6,7 +6,7 @@ on:
jobs: jobs:
check-duplicates: check-duplicates:
if: github.event.action == 'opened' && !contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.issue.author_association) if: github.event.action == 'opened'
runs-on: blacksmith-4vcpu-ubuntu-2404 runs-on: blacksmith-4vcpu-ubuntu-2404
permissions: permissions:
contents: read contents: read
@@ -118,7 +118,7 @@ jobs:
Remember: post at most ONE comment combining all findings. If everything is fine, post nothing." Remember: post at most ONE comment combining all findings. If everything is fine, post nothing."
recheck-compliance: recheck-compliance:
if: github.event.action == 'edited' && contains(github.event.issue.labels.*.name, 'needs:compliance') && !contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.issue.author_association) if: github.event.action == 'edited' && contains(github.event.issue.labels.*.name, 'needs:compliance')
runs-on: blacksmith-4vcpu-ubuntu-2404 runs-on: blacksmith-4vcpu-ubuntu-2404
permissions: permissions:
contents: read contents: read
+6 -9
View File
@@ -11,25 +11,22 @@ jobs:
contents: read contents: read
pull-requests: write pull-requests: write
steps: steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 1
- name: Check team membership - name: Check team membership
id: team-check id: team-check
run: | run: |
LOGIN="${{ github.event.pull_request.user.login }}" LOGIN="${{ github.event.pull_request.user.login }}"
ASSOCIATION="${{ github.event.pull_request.author_association }}" if [ "$LOGIN" = "opencode-agent[bot]" ] || grep -qxF "$LOGIN" .github/TEAM_MEMBERS; then
if [ "$LOGIN" = "opencode-agent[bot]" ] || [ "$ASSOCIATION" = "OWNER" ] || [ "$ASSOCIATION" = "MEMBER" ] || [ "$ASSOCIATION" = "COLLABORATOR" ]; then
echo "is_team=true" >> "$GITHUB_OUTPUT" echo "is_team=true" >> "$GITHUB_OUTPUT"
echo "Skipping: $LOGIN is a team member or bot" echo "Skipping: $LOGIN is a team member or bot"
else else
echo "is_team=false" >> "$GITHUB_OUTPUT" echo "is_team=false" >> "$GITHUB_OUTPUT"
fi fi
- name: Checkout repository
if: steps.team-check.outputs.is_team != 'true'
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 1
ref: ${{ github.event.pull_request.base.sha }}
- name: Setup Bun - name: Setup Bun
if: steps.team-check.outputs.is_team != 'true' if: steps.team-check.outputs.is_team != 'true'
uses: ./.github/actions/setup-bun uses: ./.github/actions/setup-bun
+18 -6
View File
@@ -28,9 +28,15 @@ jobs:
// Check if author is a team member or bot // Check if author is a team member or bot
if (login === 'opencode-agent[bot]') return; if (login === 'opencode-agent[bot]') return;
const teamAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR']; const { data: file } = await github.rest.repos.getContent({
if (teamAssociations.includes(pr.author_association)) { owner: context.repo.owner,
console.log(`Skipping: ${login} has author association ${pr.author_association}`); repo: context.repo.repo,
path: '.github/TEAM_MEMBERS',
ref: 'dev'
});
const members = Buffer.from(file.content, 'base64').toString().split('\n').map(l => l.trim()).filter(Boolean);
if (members.includes(login)) {
console.log(`Skipping: ${login} is a team member`);
return; return;
} }
@@ -169,9 +175,15 @@ jobs:
// Check if author is a team member or bot // Check if author is a team member or bot
if (login === 'opencode-agent[bot]') return; if (login === 'opencode-agent[bot]') return;
const teamAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR']; const { data: file } = await github.rest.repos.getContent({
if (teamAssociations.includes(pr.author_association)) { owner: context.repo.owner,
console.log(`Skipping: ${login} has author association ${pr.author_association}`); repo: context.repo.repo,
path: '.github/TEAM_MEMBERS',
ref: 'dev'
});
const members = Buffer.from(file.content, 'base64').toString().split('\n').map(l => l.trim()).filter(Boolean);
if (members.includes(login)) {
console.log(`Skipping: ${login} is a team member`);
return; return;
} }
-2
View File
@@ -1,8 +1,6 @@
- To regenerate the JavaScript SDK, run `./packages/sdk/js/script/build.ts`. - To regenerate the JavaScript SDK, run `./packages/sdk/js/script/build.ts`.
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
- The default branch in this repo is `dev`. - The default branch in this repo is `dev`.
- Local `main` ref may not exist; use `dev` or `origin/dev` for diffs. - Local `main` ref may not exist; use `dev` or `origin/dev` for diffs.
- Prefer automation: execute requested actions without confirmation unless blocked by missing info or safety/irreversibility.
## Commits and PR Titles ## Commits and PR Titles
+236 -54
View File
@@ -23,13 +23,13 @@
"oxlint-tsgolint": "0.21.0", "oxlint-tsgolint": "0.21.0",
"prettier": "3.6.2", "prettier": "3.6.2",
"semver": "^7.6.0", "semver": "^7.6.0",
"sst": "4.13.1", "sst": "catalog:",
"turbo": "2.8.13", "turbo": "2.8.13",
}, },
}, },
"packages/app": { "packages/app": {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.15.7", "version": "1.15.11",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
@@ -44,6 +44,7 @@
"@solid-primitives/i18n": "2.2.1", "@solid-primitives/i18n": "2.2.1",
"@solid-primitives/media": "2.3.3", "@solid-primitives/media": "2.3.3",
"@solid-primitives/resize-observer": "2.1.5", "@solid-primitives/resize-observer": "2.1.5",
"@solid-primitives/scheduled": "1.5.3",
"@solid-primitives/scroll": "2.1.3", "@solid-primitives/scroll": "2.1.3",
"@solid-primitives/storage": "catalog:", "@solid-primitives/storage": "catalog:",
"@solid-primitives/timer": "1.4.4", "@solid-primitives/timer": "1.4.4",
@@ -84,7 +85,7 @@
}, },
"packages/console/app": { "packages/console/app": {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.15.7", "version": "1.15.11",
"dependencies": { "dependencies": {
"@cloudflare/vite-plugin": "1.15.2", "@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",
@@ -101,6 +102,7 @@
"@solidjs/router": "catalog:", "@solidjs/router": "catalog:",
"@solidjs/start": "catalog:", "@solidjs/start": "catalog:",
"@stripe/stripe-js": "8.6.1", "@stripe/stripe-js": "8.6.1",
"@upstash/redis": "1.38.0",
"chart.js": "4.5.1", "chart.js": "4.5.1",
"nitro": "3.0.1-alpha.1", "nitro": "3.0.1-alpha.1",
"solid-js": "catalog:", "solid-js": "catalog:",
@@ -119,7 +121,7 @@
}, },
"packages/console/core": { "packages/console/core": {
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.15.7", "version": "1.15.11",
"dependencies": { "dependencies": {
"@aws-sdk/client-sts": "3.782.0", "@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1", "@jsx-email/render": "1.1.1",
@@ -146,7 +148,7 @@
}, },
"packages/console/function": { "packages/console/function": {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.15.7", "version": "1.15.11",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "3.0.64", "@ai-sdk/anthropic": "3.0.64",
"@ai-sdk/openai": "3.0.48", "@ai-sdk/openai": "3.0.48",
@@ -168,7 +170,7 @@
}, },
"packages/console/mail": { "packages/console/mail": {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.15.7", "version": "1.15.11",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
@@ -190,23 +192,43 @@
"cloudflare": "5.2.0", "cloudflare": "5.2.0",
}, },
}, },
"packages/console/support": {
"name": "@opencode-ai/console-support",
"version": "1.15.11",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@opencode-ai/console-core": "workspace:*",
"@solidjs/meta": "catalog:",
"@solidjs/router": "catalog:",
"@solidjs/start": "catalog:",
"nitro": "3.0.1-alpha.1",
"solid-js": "catalog:",
"vite": "catalog:",
},
"devDependencies": {
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
"typescript": "catalog:",
"wrangler": "4.50.0",
},
},
"packages/core": { "packages/core": {
"name": "@opencode-ai/core", "name": "@opencode-ai/core",
"version": "1.15.7", "version": "1.15.11",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
"dependencies": { "dependencies": {
"@ai-sdk/alibaba": "1.0.17", "@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/amazon-bedrock": "4.0.96", "@ai-sdk/amazon-bedrock": "4.0.107",
"@ai-sdk/anthropic": "3.0.71", "@ai-sdk/anthropic": "3.0.71",
"@ai-sdk/azure": "3.0.49", "@ai-sdk/azure": "3.0.49",
"@ai-sdk/cerebras": "2.0.41", "@ai-sdk/cerebras": "2.0.41",
"@ai-sdk/cohere": "3.0.27", "@ai-sdk/cohere": "3.0.27",
"@ai-sdk/deepinfra": "2.0.41", "@ai-sdk/deepinfra": "2.0.41",
"@ai-sdk/gateway": "3.0.104", "@ai-sdk/gateway": "3.0.104",
"@ai-sdk/google": "3.0.63", "@ai-sdk/google": "3.0.73",
"@ai-sdk/google-vertex": "4.0.112", "@ai-sdk/google-vertex": "4.0.128",
"@ai-sdk/groq": "3.0.31", "@ai-sdk/groq": "3.0.31",
"@ai-sdk/mistral": "3.0.27", "@ai-sdk/mistral": "3.0.27",
"@ai-sdk/openai": "3.0.53", "@ai-sdk/openai": "3.0.53",
@@ -238,7 +260,7 @@
"minimatch": "10.2.5", "minimatch": "10.2.5",
"npm-package-arg": "13.0.2", "npm-package-arg": "13.0.2",
"semver": "^7.6.3", "semver": "^7.6.3",
"venice-ai-sdk-provider": "2.0.1", "venice-ai-sdk-provider": "2.0.2",
"xdg-basedir": "5.1.0", "xdg-basedir": "5.1.0",
"zod": "catalog:", "zod": "catalog:",
}, },
@@ -253,7 +275,7 @@
}, },
"packages/desktop": { "packages/desktop": {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"version": "1.15.7", "version": "1.15.11",
"dependencies": { "dependencies": {
"@zip.js/zip.js": "2.7.62", "@zip.js/zip.js": "2.7.62",
"drizzle-orm": "catalog:", "drizzle-orm": "catalog:",
@@ -308,7 +330,7 @@
}, },
"packages/effect-drizzle-sqlite": { "packages/effect-drizzle-sqlite": {
"name": "@opencode-ai/effect-drizzle-sqlite", "name": "@opencode-ai/effect-drizzle-sqlite",
"version": "1.15.7", "version": "1.15.11",
"dependencies": { "dependencies": {
"drizzle-orm": "catalog:", "drizzle-orm": "catalog:",
"effect": "catalog:", "effect": "catalog:",
@@ -322,7 +344,7 @@
}, },
"packages/enterprise": { "packages/enterprise": {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.15.7", "version": "1.15.11",
"dependencies": { "dependencies": {
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
@@ -352,7 +374,7 @@
}, },
"packages/function": { "packages/function": {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.15.7", "version": "1.15.11",
"dependencies": { "dependencies": {
"@octokit/auth-app": "8.0.1", "@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:", "@octokit/rest": "catalog:",
@@ -368,7 +390,7 @@
}, },
"packages/http-recorder": { "packages/http-recorder": {
"name": "@opencode-ai/http-recorder", "name": "@opencode-ai/http-recorder",
"version": "1.15.7", "version": "1.15.11",
"dependencies": { "dependencies": {
"@effect/platform-node": "catalog:", "@effect/platform-node": "catalog:",
"effect": "catalog:", "effect": "catalog:",
@@ -381,7 +403,7 @@
}, },
"packages/llm": { "packages/llm": {
"name": "@opencode-ai/llm", "name": "@opencode-ai/llm",
"version": "1.15.7", "version": "1.15.11",
"dependencies": { "dependencies": {
"@smithy/eventstream-codec": "4.2.14", "@smithy/eventstream-codec": "4.2.14",
"@smithy/util-utf8": "4.2.2", "@smithy/util-utf8": "4.2.2",
@@ -399,7 +421,7 @@
}, },
"packages/opencode": { "packages/opencode": {
"name": "opencode", "name": "opencode",
"version": "1.15.7", "version": "1.15.11",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@@ -408,15 +430,15 @@
"@actions/github": "6.0.1", "@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.21.0", "@agentclientprotocol/sdk": "0.21.0",
"@ai-sdk/alibaba": "1.0.17", "@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/amazon-bedrock": "4.0.96", "@ai-sdk/amazon-bedrock": "4.0.107",
"@ai-sdk/anthropic": "3.0.71", "@ai-sdk/anthropic": "3.0.71",
"@ai-sdk/azure": "3.0.49", "@ai-sdk/azure": "3.0.49",
"@ai-sdk/cerebras": "2.0.41", "@ai-sdk/cerebras": "2.0.41",
"@ai-sdk/cohere": "3.0.27", "@ai-sdk/cohere": "3.0.27",
"@ai-sdk/deepinfra": "2.0.41", "@ai-sdk/deepinfra": "2.0.41",
"@ai-sdk/gateway": "3.0.104", "@ai-sdk/gateway": "3.0.104",
"@ai-sdk/google": "3.0.63", "@ai-sdk/google": "3.0.73",
"@ai-sdk/google-vertex": "4.0.112", "@ai-sdk/google-vertex": "4.0.128",
"@ai-sdk/groq": "3.0.31", "@ai-sdk/groq": "3.0.31",
"@ai-sdk/mistral": "3.0.27", "@ai-sdk/mistral": "3.0.27",
"@ai-sdk/openai": "3.0.53", "@ai-sdk/openai": "3.0.53",
@@ -456,6 +478,7 @@
"@solid-primitives/event-bus": "1.1.2", "@solid-primitives/event-bus": "1.1.2",
"@solid-primitives/scheduled": "1.5.2", "@solid-primitives/scheduled": "1.5.2",
"@standard-schema/spec": "1.0.0", "@standard-schema/spec": "1.0.0",
"@types/ws": "8.18.1",
"@zip.js/zip.js": "2.7.62", "@zip.js/zip.js": "2.7.62",
"ai": "catalog:", "ai": "catalog:",
"ai-gateway-provider": "3.1.2", "ai-gateway-provider": "3.1.2",
@@ -493,10 +516,11 @@
"tree-sitter-powershell": "0.25.10", "tree-sitter-powershell": "0.25.10",
"turndown": "7.2.0", "turndown": "7.2.0",
"ulid": "catalog:", "ulid": "catalog:",
"venice-ai-sdk-provider": "2.0.1", "venice-ai-sdk-provider": "2.0.2",
"vscode-jsonrpc": "8.2.1", "vscode-jsonrpc": "8.2.1",
"web-tree-sitter": "0.25.10", "web-tree-sitter": "0.25.10",
"which": "6.0.1", "which": "6.0.1",
"ws": "8.21.0",
"xdg-basedir": "5.1.0", "xdg-basedir": "5.1.0",
"yargs": "18.0.0", "yargs": "18.0.0",
"zod": "catalog:", "zod": "catalog:",
@@ -537,7 +561,7 @@
}, },
"packages/plugin": { "packages/plugin": {
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.15.7", "version": "1.15.11",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"effect": "catalog:", "effect": "catalog:",
@@ -553,9 +577,9 @@
"typescript": "catalog:", "typescript": "catalog:",
}, },
"peerDependencies": { "peerDependencies": {
"@opentui/core": ">=0.2.15", "@opentui/core": ">=0.2.16",
"@opentui/keymap": ">=0.2.15", "@opentui/keymap": ">=0.2.16",
"@opentui/solid": ">=0.2.15", "@opentui/solid": ">=0.2.16",
}, },
"optionalPeers": [ "optionalPeers": [
"@opentui/core", "@opentui/core",
@@ -575,7 +599,7 @@
}, },
"packages/sdk/js": { "packages/sdk/js": {
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "1.15.7", "version": "1.15.11",
"dependencies": { "dependencies": {
"cross-spawn": "catalog:", "cross-spawn": "catalog:",
}, },
@@ -590,7 +614,7 @@
}, },
"packages/slack": { "packages/slack": {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.15.7", "version": "1.15.11",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1", "@slack/bolt": "^3.17.1",
@@ -601,6 +625,67 @@
"typescript": "catalog:", "typescript": "catalog:",
}, },
}, },
"packages/stats/app": {
"name": "@opencode-ai/stats-app",
"version": "1.15.11",
"dependencies": {
"@ibm/plex": "6.4.1",
"@opencode-ai/stats-core": "workspace:*",
"@opencode-ai/ui": "workspace:*",
"@solidjs/meta": "catalog:",
"@solidjs/router": "catalog:",
"@solidjs/start": "catalog:",
"d3-scale": "4.0.2",
"effect": "catalog:",
"nitro": "3.0.1-alpha.1",
"solid-js": "catalog:",
"vite": "catalog:",
},
"devDependencies": {
"@cloudflare/workers-types": "catalog:",
"@types/bun": "catalog:",
"@types/d3-scale": "4.0.9",
"@typescript/native-preview": "catalog:",
"typescript": "catalog:",
},
},
"packages/stats/core": {
"name": "@opencode-ai/stats-core",
"version": "1.15.11",
"dependencies": {
"@aws-sdk/client-athena": "3.933.0",
"@planetscale/database": "1.19.0",
"drizzle-orm": "catalog:",
"effect": "catalog:",
"sst": "catalog:",
},
"devDependencies": {
"@tsconfig/node22": "catalog:",
"@types/bun": "catalog:",
"@types/node": "catalog:",
"@typescript/native-preview": "catalog:",
"drizzle-kit": "catalog:",
"typescript": "catalog:",
},
},
"packages/stats/server": {
"name": "@opencode-ai/stats-server",
"version": "1.15.11",
"dependencies": {
"@aws-sdk/client-firehose": "3.933.0",
"@effect/platform-node": "catalog:",
"@opencode-ai/stats-core": "workspace:*",
"effect": "catalog:",
"sst": "catalog:",
},
"devDependencies": {
"@tsconfig/node22": "catalog:",
"@types/bun": "catalog:",
"@types/node": "catalog:",
"@typescript/native-preview": "catalog:",
"typescript": "catalog:",
},
},
"packages/storybook": { "packages/storybook": {
"name": "@opencode-ai/storybook", "name": "@opencode-ai/storybook",
"devDependencies": { "devDependencies": {
@@ -625,7 +710,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "1.15.7", "version": "1.15.11",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*", "@opencode-ai/core": "workspace:*",
@@ -674,7 +759,7 @@
}, },
"packages/web": { "packages/web": {
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"version": "1.15.7", "version": "1.15.11",
"dependencies": { "dependencies": {
"@astrojs/cloudflare": "12.6.3", "@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1", "@astrojs/markdown-remark": "6.3.1",
@@ -717,6 +802,8 @@
], ],
"patchedDependencies": { "patchedDependencies": {
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch", "solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
"virtua@0.49.1": "patches/virtua@0.49.1.patch",
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
"@ai-sdk/xai@3.0.82": "patches/@ai-sdk%2Fxai@3.0.82.patch", "@ai-sdk/xai@3.0.82": "patches/@ai-sdk%2Fxai@3.0.82.patch",
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
"@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch", "@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch",
@@ -740,9 +827,9 @@
"@npmcli/arborist": "9.4.0", "@npmcli/arborist": "9.4.0",
"@octokit/rest": "22.0.0", "@octokit/rest": "22.0.0",
"@openauthjs/openauth": "0.0.0-20250322224806", "@openauthjs/openauth": "0.0.0-20250322224806",
"@opentui/core": "0.2.15", "@opentui/core": "0.2.16",
"@opentui/keymap": "0.2.15", "@opentui/keymap": "0.2.16",
"@opentui/solid": "0.2.15", "@opentui/solid": "0.2.16",
"@pierre/diffs": "1.1.0-beta.18", "@pierre/diffs": "1.1.0-beta.18",
"@playwright/test": "1.59.1", "@playwright/test": "1.59.1",
"@sentry/solid": "10.36.0", "@sentry/solid": "10.36.0",
@@ -780,6 +867,7 @@
"shiki": "3.20.0", "shiki": "3.20.0",
"solid-js": "1.9.10", "solid-js": "1.9.10",
"solid-list": "0.3.0", "solid-list": "0.3.0",
"sst": "4.13.1",
"tailwindcss": "4.1.11", "tailwindcss": "4.1.11",
"typescript": "5.8.2", "typescript": "5.8.2",
"ulid": "3.0.1", "ulid": "3.0.1",
@@ -809,7 +897,7 @@
"@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="], "@ai-sdk/alibaba": ["@ai-sdk/alibaba@1.0.17", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZbE+U5bWz2JBc5DERLowx5+TKbjGBE93LqKZAWvuEn7HOSQMraxFMZuc0ST335QZJAyfBOzh7m1mPQ+y7EaaoA=="],
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.96", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Mc4Ias2jRMD1jOB6xWtKNPdhECeuCZyIlbr9EAGfBnyBt++sS13ziZh9qv9TdyMCAZJ7xoQcpbchoRJcKwPdpA=="], "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.107", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.78", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8nT08pGPy25rleJNk56ep00UHK6kCtCmu+ZNqVVSSPDieADlIZqcaN1iRXAFBoCH0Fb9F6C2EjFDaySdsargfQ=="],
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="], "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="],
@@ -831,9 +919,9 @@
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.104", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA=="], "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.104", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA=="],
"@ai-sdk/google": ["@ai-sdk/google@3.0.63", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RfOZWVMYSPu2sPRfGajrauWAZ9BSaRopSn+AszkKWQ1MFj8nhaXvCqRHB5pBQUaHTfZKagvOmMpNfa/s3gPLgQ=="], "@ai-sdk/google": ["@ai-sdk/google@3.0.73", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-o2MuIeyvZrFIeIbnbA8Thrr63irdyUBh0uWBZ2lY6yFeXuE/tcwyXF74bDKS4KvTu84uFpQfpbS/LXHGKKXz+g=="],
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.112", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/google": "3.0.64", "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cSfHCkM+9ZrFtQWIN1WlV93JPD+isGSdFxKj7u1L9m2aLVZajlXdcE41GL9hMt7ld7bZYE4NnZ+4VLxBAHE+Eg=="], "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.128", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.77", "@ai-sdk/google": "3.0.73", "@ai-sdk/openai-compatible": "2.0.47", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jK8fixb4km2yfgvb9DUFQRpV/jiDB0v9gyxHoHfPydaQvz+CpAz8DTt1quyaM+Wg9G2R8Zo68CYmHbIkUqW2AA=="],
"@ai-sdk/groq": ["@ai-sdk/groq@3.0.31", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XbbugpnFmXGu2TlXiq8KUJskP6/VVbuFcnFIGDzDIB/Chg6XHsNnqrTF80Zxkh0Pd3+NvbM+2Uqrtsndk6bDAg=="], "@ai-sdk/groq": ["@ai-sdk/groq@3.0.31", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XbbugpnFmXGu2TlXiq8KUJskP6/VVbuFcnFIGDzDIB/Chg6XHsNnqrTF80Zxkh0Pd3+NvbM+2Uqrtsndk6bDAg=="],
@@ -905,8 +993,12 @@
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
"@aws-sdk/client-athena": ["@aws-sdk/client-athena@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-node": "3.933.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-9eMUCu1Ay3C9ojo+dJcynSdpbxuwDVtZUt/Xhce+c2+mgDsmvRzjww+wfLpZwRNWxBWmeauQQAZk52tCwQgXsQ=="],
"@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.993.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/credential-provider-node": "^3.972.10", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.11", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.993.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.9", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.16", "@smithy/middleware-retry": "^4.4.33", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.32", "@smithy/util-defaults-mode-node": "^4.2.35", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7Ne3Yk/bgQPVebAkv7W+RfhiwTRSbfER9BtbhOa2w/+dIr902LrJf6vrZlxiqaJbGj2ALx8M+ZK1YIHVxSwu9A=="], "@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.993.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/credential-provider-node": "^3.972.10", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.11", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.993.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.9", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.16", "@smithy/middleware-retry": "^4.4.33", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.5", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.32", "@smithy/util-defaults-mode-node": "^4.2.35", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7Ne3Yk/bgQPVebAkv7W+RfhiwTRSbfER9BtbhOa2w/+dIr902LrJf6vrZlxiqaJbGj2ALx8M+ZK1YIHVxSwu9A=="],
"@aws-sdk/client-firehose": ["@aws-sdk/client-firehose@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-node": "3.933.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-tDrtgczN2lQsflLDPYu/wdOoyCZLVYtgzmWnYzSEOBWd/cp2AbuQ7D+FemSwUTzyoMTuhhIevyEJKzqsF+QYxA=="],
"@aws-sdk/client-lambda": ["@aws-sdk/client-lambda@3.1048.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.11", "@aws-sdk/credential-provider-node": "^3.972.42", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-ryEYNVdilyWkKsOs/7Xy/l7+qjtSz4sll8NpcWD6AtONxjG/5OMaAhxxDkQb4iBoNMKnISxsARzQAp/Wa8pXIg=="], "@aws-sdk/client-lambda": ["@aws-sdk/client-lambda@3.1048.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.11", "@aws-sdk/credential-provider-node": "^3.972.42", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-ryEYNVdilyWkKsOs/7Xy/l7+qjtSz4sll8NpcWD6AtONxjG/5OMaAhxxDkQb4iBoNMKnISxsARzQAp/Wa8pXIg=="],
"@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.933.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-node": "3.933.0", "@aws-sdk/middleware-bucket-endpoint": "3.930.0", "@aws-sdk/middleware-expect-continue": "3.930.0", "@aws-sdk/middleware-flexible-checksums": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-location-constraint": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-sdk-s3": "3.932.0", "@aws-sdk/middleware-ssec": "3.930.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/signature-v4-multi-region": "3.932.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/eventstream-serde-browser": "^4.2.5", "@smithy/eventstream-serde-config-resolver": "^4.3.5", "@smithy/eventstream-serde-node": "^4.2.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-blob-browser": "^4.2.6", "@smithy/hash-node": "^4.2.5", "@smithy/hash-stream-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/md5-js": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-KxwZvdxdCeWK6o8mpnb+kk7Kgb8V+8AjTwSXUWH1UAD85B0tjdo1cSfE5zoR5fWGol4Ml5RLez12a6LPhsoTqA=="], "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.933.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/credential-provider-node": "3.933.0", "@aws-sdk/middleware-bucket-endpoint": "3.930.0", "@aws-sdk/middleware-expect-continue": "3.930.0", "@aws-sdk/middleware-flexible-checksums": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-location-constraint": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-sdk-s3": "3.932.0", "@aws-sdk/middleware-ssec": "3.930.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/signature-v4-multi-region": "3.932.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/eventstream-serde-browser": "^4.2.5", "@smithy/eventstream-serde-config-resolver": "^4.3.5", "@smithy/eventstream-serde-node": "^4.2.5", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-blob-browser": "^4.2.6", "@smithy/hash-node": "^4.2.5", "@smithy/hash-stream-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/md5-js": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "@smithy/util-waiter": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-KxwZvdxdCeWK6o8mpnb+kk7Kgb8V+8AjTwSXUWH1UAD85B0tjdo1cSfE5zoR5fWGol4Ml5RLez12a6LPhsoTqA=="],
@@ -1563,6 +1655,8 @@
"@opencode-ai/console-resource": ["@opencode-ai/console-resource@workspace:packages/console/resource"], "@opencode-ai/console-resource": ["@opencode-ai/console-resource@workspace:packages/console/resource"],
"@opencode-ai/console-support": ["@opencode-ai/console-support@workspace:packages/console/support"],
"@opencode-ai/core": ["@opencode-ai/core@workspace:packages/core"], "@opencode-ai/core": ["@opencode-ai/core@workspace:packages/core"],
"@opencode-ai/desktop": ["@opencode-ai/desktop@workspace:packages/desktop"], "@opencode-ai/desktop": ["@opencode-ai/desktop@workspace:packages/desktop"],
@@ -1585,6 +1679,12 @@
"@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"], "@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"],
"@opencode-ai/stats-app": ["@opencode-ai/stats-app@workspace:packages/stats/app"],
"@opencode-ai/stats-core": ["@opencode-ai/stats-core@workspace:packages/stats/core"],
"@opencode-ai/stats-server": ["@opencode-ai/stats-server@workspace:packages/stats/server"],
"@opencode-ai/storybook": ["@opencode-ai/storybook@workspace:packages/storybook"], "@opencode-ai/storybook": ["@opencode-ai/storybook@workspace:packages/storybook"],
"@opencode-ai/ui": ["@opencode-ai/ui@workspace:packages/ui"], "@opencode-ai/ui": ["@opencode-ai/ui@workspace:packages/ui"],
@@ -1619,23 +1719,23 @@
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="],
"@opentui/core": ["@opentui/core@0.2.15", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.15", "@opentui/core-darwin-x64": "0.2.15", "@opentui/core-linux-arm64": "0.2.15", "@opentui/core-linux-x64": "0.2.15", "@opentui/core-win32-arm64": "0.2.15", "@opentui/core-win32-x64": "0.2.15" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-YGHttdZWScMcSvtYgZkLR6VhUO1OoUiQzwYjZgIusf5eCkPLD8PapH+PTMVqAiX16CHO6JxfMlkHv5qDiHAccQ=="], "@opentui/core": ["@opentui/core@0.2.16", "", { "dependencies": { "bun-ffi-structs": "0.2.2", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.2.16", "@opentui/core-darwin-x64": "0.2.16", "@opentui/core-linux-arm64": "0.2.16", "@opentui/core-linux-x64": "0.2.16", "@opentui/core-win32-arm64": "0.2.16", "@opentui/core-win32-x64": "0.2.16" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-4vWN15Zc3nsXJlOiHhhpqkBXD+wrNFKxCPtiTiillZYDRre+XsZogVTOOGUDwaBIC23OSxq7imezLmmtShVBEA=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-s25f9GmZd6wxNM5ExRmwwnLT+NLCKxnTWuO9aObOlqsXfLMGHQZrb6YwgAn/PSTua98KmH7GJCVWdPgZ/P+0RQ=="], "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.2.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aFb2Yp+oqDu3h6VCWi7xpQ9yjpKSQcROzGGfHgqC6Nd3U+uiLfPJBkmiI87iK0opCggCFj5TkKI004050DmGjg=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-GyaipN+nOcEr8rcTO2mqKTGmOBk0C300I69fLtubD3BadHcMI1DVNlQrcf/J1mkQEuMYbmBTi/1hT1ybWGr2Mw=="], "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.2.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-KimiHE0j7EsTB5P8doW0lr1eH5iZKLPKWQO+tmy1VcdYr/TzqhdHSvGuJXrZvfTFi9/rV57Eq0d7964Ri9O0vQ=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-h+uyufselGT4afKMP8Lg4yUl5Kp+DJBlhu3XpWXhphE5Pnq5+f0uGBr4P+34CNcWxMsDnvagSQLFRCS4rGrOWA=="], "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.2.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-4fCwRCfTtUgS/5QcSEkSuBjgQymSOUWXgrXG2ycrf3Swi0QhKDA/pVjwLrUJ6eF+/8mQyQSEV72T8MxMO3M2qg=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.15", "", { "os": "linux", "cpu": "x64" }, "sha512-jx+NImPq4wSp3Apfe7tlixiEJNnRyECTRJRWhGF6ZJz4PwFfgK2UHZKYR0DZHbV8nYawoDNQPJDXEWcoZShnMg=="], "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.2.16", "", { "os": "linux", "cpu": "x64" }, "sha512-KgQBGjiucw4e7gM+R8qOzHWBFhjCY1IfCrGjW3Wzxv2hKUlL+mPhelaeJwnEqtNxMUdVTYjlwlu3IHxslXMJWQ=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-2SQQLvf3sgmToxrNika9AdcccKrjPJEn5jW6sSv0oEixNBzUzW41vSZZG4LM/V3lL8eg0LoYDnRZeKLB4gwSqQ=="], "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.2.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-C6WqEI3VkXatXraMgSFXZjEXq0pzURGjRpFAJZYmuVDmpqE57o7E80Np2UkdZ6m5kpJDt4mRyu3krc/P825iNQ=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.15", "", { "os": "win32", "cpu": "x64" }, "sha512-SVMVgnC7LVEm+yVZKdmmhRBj/xAT94PanT+UCcHxaCWK+OLmv/AX+ohHq2m0odup6iXcEqj+7mAltO9fgJLFIg=="], "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.2.16", "", { "os": "win32", "cpu": "x64" }, "sha512-kCX3CMTns6DMCFDNTDV4sjmBKyA/iEvzaVhl/jYi4JRIVT2zcy1lo+lhXT5mPgYHmJZu8Uye6j3Zi3c7Z2Me5A=="],
"@opentui/keymap": ["@opentui/keymap@0.2.15", "", { "dependencies": { "@opentui/core": "0.2.15" }, "peerDependencies": { "@opentui/react": "0.2.15", "@opentui/solid": "0.2.15", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-wqQp6y7P2jZZJiOMwupxjGryuSWCs+njjglwW/xny9J17gomBmUvTIcIIWNG0Jv+EGO9ScBzCScGlwBHFhHyYw=="], "@opentui/keymap": ["@opentui/keymap@0.2.16", "", { "dependencies": { "@opentui/core": "0.2.16" }, "peerDependencies": { "@opentui/react": "0.2.16", "@opentui/solid": "0.2.16", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-YBLQfNLbU2kx49bjEY9rrFoNlvIoi5qNJfRcOt6frvnR3C6MLl0/8hZY+vMQ2PEQWeEiNejFnl1lQw+z4Nk2FQ=="],
"@opentui/solid": ["@opentui/solid@0.2.15", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.15", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-CViepAjsCWXwrLndMt+qlLo7cooVX7DXwSJHNizw7mfrRJtOPzSYJZCIk1vF4IJTWffCHygoYMe3uSeKvzAcbw=="], "@opentui/solid": ["@opentui/solid@0.2.16", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.2.16", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-2Q+v1PPpXXr+sALi9Aj6I5Jvo7xDfbmstYjRLL7lW3Hghh9i7ONQKpt/gyDDRbhSsYrhxKYTNenF9OxgoXkTHg=="],
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
@@ -2177,7 +2277,7 @@
"@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-N8cIDAHbWcLahNRLr0knAAQvXyEdEMoAZvIMZKmhNb1mlx9e2UOv9BRD5YNwQUJwbNoYVhhLwFOEOcVXFx0HqA=="], "@solid-primitives/rootless": ["@solid-primitives/rootless@1.5.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-N8cIDAHbWcLahNRLr0knAAQvXyEdEMoAZvIMZKmhNb1mlx9e2UOv9BRD5YNwQUJwbNoYVhhLwFOEOcVXFx0HqA=="],
"@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-/j2igE0xyNaHhj6kMfcUQn5rAVSTLbAX+CDEBm25hSNBmNiHLu2lM7Usj2kJJ5j36D67bE8wR1hBNA8hjtvsQA=="], "@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.3", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-oNwLE6E6lxJAWrc8QXuwM0k2oU1BnANnkChwMw82aK1j3+mWGJkG1IFe5gCwbV+afYmjI76t9JJV3md/8tLw+g=="],
"@solid-primitives/scroll": ["@solid-primitives/scroll@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Ejq/Z7zKo/6eIEFr1bFLzXFxiGBCMLuqCM8QB8urr3YdPzjSETFLzYRWUyRiDWaBQN0F7k0SY6S7ig5nWOP7vg=="], "@solid-primitives/scroll": ["@solid-primitives/scroll@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Ejq/Z7zKo/6eIEFr1bFLzXFxiGBCMLuqCM8QB8urr3YdPzjSETFLzYRWUyRiDWaBQN0F7k0SY6S7ig5nWOP7vg=="],
@@ -2323,6 +2423,10 @@
"@types/cross-spawn": ["@types/cross-spawn@6.0.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA=="], "@types/cross-spawn": ["@types/cross-spawn@6.0.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA=="],
"@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
"@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
"@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
@@ -2467,6 +2571,8 @@
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
"@upstash/redis": ["@upstash/redis@1.38.0", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-wu+dZBptlLy0+MCUEoHmzrY/TnmgDey3+c7EbIGwrLqAvkP8yi5MWZHYGIFtAygmL4Bkz2TdFu+eU0vFPncIcg=="],
"@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="], "@valibot/to-json-schema": ["@valibot/to-json-schema@1.6.0", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="],
"@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="],
@@ -2893,6 +2999,20 @@
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
"d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
"d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="],
"d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
"d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
"d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
"d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
@@ -3467,6 +3587,8 @@
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
"ioredis": ["ioredis@5.10.1", "", { "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA=="], "ioredis": ["ioredis@5.10.1", "", { "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA=="],
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
@@ -4911,7 +5033,7 @@
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"venice-ai-sdk-provider": ["venice-ai-sdk-provider@2.0.1", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.37", "@ai-sdk/provider": "^3.0.8", "@ai-sdk/provider-utils": "^4.0.21" }, "peerDependencies": { "ai": "^6.0.90" } }, "sha512-6SxA8a4MoA6Q/c+D3q7My0Hfog76enN3n0MXhwosM+tso66rXBEGeBRD/0lravRDVzL2Q1w5QJPc86rAVJtfXg=="], "venice-ai-sdk-provider": ["venice-ai-sdk-provider@2.0.2", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.47", "@ai-sdk/provider": "^3.0.10", "@ai-sdk/provider-utils": "^4.0.27" }, "peerDependencies": { "ai": "^6.0.90" } }, "sha512-aoa05nI3BTK5aGbjBflq+Gfln2AHAkwNbWuGGvCzUIsOfp5Y3iPD4O4PUGDAEiWVJWbjpPn0KfDa0H/HebwsaA=="],
"verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="], "verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="],
@@ -5019,7 +5141,7 @@
"write-file-atomic": ["write-file-atomic@7.0.1", "", { "dependencies": { "signal-exit": "^4.0.1" } }, "sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg=="], "write-file-atomic": ["write-file-atomic@7.0.1", "", { "dependencies": { "signal-exit": "^4.0.1" } }, "sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg=="],
"ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
"wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
@@ -5089,9 +5211,13 @@
"@ai-sdk/alibaba/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], "@ai-sdk/alibaba/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
"@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="], "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.78", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0OY12G20cUt6iU6htpEA1491Oz++NVxZxlmWGX4B7rSbeZ5pnDmOu6YtW9BKzdZlNx5Gn23i6WMxyZFoMKNcgA=="],
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.13", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.0", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-vYahwBAtRaAcFbOmE9aLr12z7RiHYDSLcnogSdxfm7kKfsNa3wH+NU5r7vTeB5rKvLsWyPjVX8iH94brP7umiQ=="], "@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="],
"@ai-sdk/amazon-bedrock/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], "@ai-sdk/amazon-bedrock/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
@@ -5107,11 +5233,17 @@
"@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], "@ai-sdk/fireworks/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="], "@ai-sdk/google/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
"@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CbR82EgGPNrj/6q0HtclwuCqe0/pDShyv3nWDP/A9DroujzWXnLMlUJVrgPOsg4b40zQCwwVs2XSKCxvt/4QaA=="], "@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
"@ai-sdk/google-vertex/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], "@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.77", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ML8C2M1YvPA1ulEx4TiyF0k1xvC2ikEiPBIC1PPQ0a5xELUGrO2lAaEzsTEoJ+eCeDd8PSBuFJjs+r+9yIwQXA=="],
"@ai-sdk/google-vertex/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Enm5UlL0zUCrW3792opk5h7hRWxZOZzDe6eQYVFqX9LUOGGCe1h8MZWAGim765nwzgnjlpeYOsuzZmLtRsTPlg=="],
"@ai-sdk/google-vertex/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
"@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
"@ai-sdk/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], "@ai-sdk/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
@@ -5165,6 +5297,16 @@
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
"@aws-sdk/client-athena/@smithy/core": ["@smithy/core@3.24.3", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg=="],
"@aws-sdk/client-athena/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.4.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A=="],
"@aws-sdk/client-athena/@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA=="],
"@aws-sdk/client-athena/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="],
"@aws-sdk/client-athena/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@aws-sdk/client-cognito-identity/@aws-sdk/core": ["@aws-sdk/core@3.973.27", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws-sdk/xml-builder": "^3.972.17", "@smithy/core": "^3.23.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/property-provider": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/signature-v4": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A=="], "@aws-sdk/client-cognito-identity/@aws-sdk/core": ["@aws-sdk/core@3.973.27", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws-sdk/xml-builder": "^3.972.17", "@smithy/core": "^3.23.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/property-provider": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/signature-v4": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A=="],
"@aws-sdk/client-cognito-identity/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.30", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.25", "@aws-sdk/credential-provider-http": "^3.972.27", "@aws-sdk/credential-provider-ini": "^3.972.29", "@aws-sdk/credential-provider-process": "^3.972.25", "@aws-sdk/credential-provider-sso": "^3.972.29", "@aws-sdk/credential-provider-web-identity": "^3.972.29", "@aws-sdk/types": "^3.973.7", "@smithy/credential-provider-imds": "^4.2.13", "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-FMnAnWxc8PG+ZrZ2OBKzY4luCUJhe9CG0B9YwYr4pzrYGLXBS2rl+UoUvjGbAwiptxRL6hyA3lFn03Bv1TLqTw=="], "@aws-sdk/client-cognito-identity/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.30", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.25", "@aws-sdk/credential-provider-http": "^3.972.27", "@aws-sdk/credential-provider-ini": "^3.972.29", "@aws-sdk/credential-provider-process": "^3.972.25", "@aws-sdk/credential-provider-sso": "^3.972.29", "@aws-sdk/credential-provider-web-identity": "^3.972.29", "@aws-sdk/types": "^3.973.7", "@smithy/credential-provider-imds": "^4.2.13", "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-FMnAnWxc8PG+ZrZ2OBKzY4luCUJhe9CG0B9YwYr4pzrYGLXBS2rl+UoUvjGbAwiptxRL6hyA3lFn03Bv1TLqTw=="],
@@ -5189,6 +5331,16 @@
"@aws-sdk/client-cognito-identity/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], "@aws-sdk/client-cognito-identity/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@aws-sdk/client-firehose/@smithy/core": ["@smithy/core@3.24.3", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg=="],
"@aws-sdk/client-firehose/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.4.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A=="],
"@aws-sdk/client-firehose/@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA=="],
"@aws-sdk/client-firehose/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="],
"@aws-sdk/client-firehose/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
"@aws-sdk/client-lambda/@aws-sdk/core": ["@aws-sdk/core@3.974.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/xml-builder": "^3.972.24", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.2", "@smithy/signature-v4": "^5.4.2", "@smithy/types": "^4.14.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-QpnINq5FZH6EOaDEkmHdT7eUunbvD27pDNQypaWjFyYz7Zl1q3UCMQErBZxpmfGfI7MvI2TlK8KTkgNpv8b1ug=="], "@aws-sdk/client-lambda/@aws-sdk/core": ["@aws-sdk/core@3.974.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/xml-builder": "^3.972.24", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.2", "@smithy/signature-v4": "^5.4.2", "@smithy/types": "^4.14.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-QpnINq5FZH6EOaDEkmHdT7eUunbvD27pDNQypaWjFyYz7Zl1q3UCMQErBZxpmfGfI7MvI2TlK8KTkgNpv8b1ug=="],
"@aws-sdk/client-lambda/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.42", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.37", "@aws-sdk/credential-provider-http": "^3.972.39", "@aws-sdk/credential-provider-ini": "^3.972.41", "@aws-sdk/credential-provider-process": "^3.972.37", "@aws-sdk/credential-provider-sso": "^3.972.41", "@aws-sdk/credential-provider-web-identity": "^3.972.41", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/credential-provider-imds": "^4.3.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-D4oon2zbqqsWOJUM99Gm3/ZyJ0IJvTXVN3PyloGb3kQEyI36fjCZheZj422lAgTWWd6TSHgiImLt3RIaLdv3dQ=="], "@aws-sdk/client-lambda/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.42", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.37", "@aws-sdk/credential-provider-http": "^3.972.39", "@aws-sdk/credential-provider-ini": "^3.972.41", "@aws-sdk/credential-provider-process": "^3.972.37", "@aws-sdk/credential-provider-sso": "^3.972.41", "@aws-sdk/credential-provider-web-identity": "^3.972.41", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/credential-provider-imds": "^4.3.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-D4oon2zbqqsWOJUM99Gm3/ZyJ0IJvTXVN3PyloGb3kQEyI36fjCZheZj422lAgTWWd6TSHgiImLt3RIaLdv3dQ=="],
@@ -5347,6 +5499,8 @@
"@cloudflare/kv-asset-handler/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], "@cloudflare/kv-asset-handler/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
"@cloudflare/vite-plugin/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
"@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
"@develar/schema-utils/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], "@develar/schema-utils/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
@@ -5809,6 +5963,8 @@
"miniflare/undici": ["undici@7.14.0", "", {}, "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ=="], "miniflare/undici": ["undici@7.14.0", "", {}, "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ=="],
"miniflare/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
"miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="], "miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
"minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
@@ -5837,6 +5993,8 @@
"opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], "opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
"opencode/@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-/j2igE0xyNaHhj6kMfcUQn5rAVSTLbAX+CDEBm25hSNBmNiHLu2lM7Usj2kJJ5j36D67bE8wR1hBNA8hjtvsQA=="],
"opencode/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="], "opencode/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
"opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], "opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
@@ -5923,6 +6081,8 @@
"storybook/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], "storybook/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
"storybook/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
"storybook-solidjs-vite/vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="], "storybook-solidjs-vite/vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="],
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
@@ -5953,7 +6113,11 @@
"unused-filename/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="], "unused-filename/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="],
"venice-ai-sdk-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], "venice-ai-sdk-provider/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.47", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Enm5UlL0zUCrW3792opk5h7hRWxZOZzDe6eQYVFqX9LUOGGCe1h8MZWAGim765nwzgnjlpeYOsuzZmLtRsTPlg=="],
"venice-ai-sdk-provider/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
"venice-ai-sdk-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="],
"vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], "vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
@@ -5999,6 +6163,12 @@
"@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], "@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
"@ai-sdk/amazon-bedrock/@smithy/eventstream-codec/@smithy/types": ["@smithy/types@4.14.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg=="],
"@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
@@ -6009,6 +6179,14 @@
"@ai-sdk/deepinfra/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@ai-sdk/deepinfra/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/google-vertex/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/google-vertex/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
"@ai-sdk/google/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/google/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
"@ai-sdk/groq/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@ai-sdk/groq/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/mistral/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@ai-sdk/mistral/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
@@ -6655,6 +6833,10 @@
"unplugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], "unplugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"venice-ai-sdk-provider/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"venice-ai-sdk-provider/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
"vitest/@vitest/expect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "vitest/@vitest/expect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], "vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
+1 -1
View File
@@ -30,7 +30,7 @@ export const api = new sst.cloudflare.Worker("Api", {
transform: { transform: {
worker: (args) => { worker: (args) => {
args.logpush = true args.logpush = true
if ($app.stage === "vimtor") return if ($app.stage === "vimtor" || $app.stage === "adam") return
args.bindings = $resolve(args.bindings).apply((bindings) => [ args.bindings = $resolve(args.bindings).apply((bindings) => [
...bindings, ...bindings,
{ {
+7 -3
View File
@@ -1,7 +1,9 @@
import { domain } from "./stage" import { deployAws, domain } from "./stage"
import { EMAILOCTOPUS_API_KEY } from "./app" import { EMAILOCTOPUS_API_KEY } from "./app"
import { SECRET } from "./secret" import { SECRET } from "./secret"
const lake = deployAws ? await import("./lake") : undefined
//////////////// ////////////////
// DATABASE // DATABASE
//////////////// ////////////////
@@ -240,7 +242,7 @@ const SALESFORCE_INSTANCE_URL = new sst.Secret("SALESFORCE_INSTANCE_URL")
const logProcessor = new sst.cloudflare.Worker("LogProcessor", { const logProcessor = new sst.cloudflare.Worker("LogProcessor", {
handler: "packages/console/function/src/log-processor.ts", handler: "packages/console/function/src/log-processor.ts",
link: [new sst.Secret("HONEYCOMB_API_KEY")], link: [SECRET.HoneycombApiKey, ...(lake?.lakeIngest ? [lake.lakeIngest] : [])],
}) })
new sst.cloudflare.x.SolidStart("Console", { new sst.cloudflare.x.SolidStart("Console", {
@@ -250,6 +252,8 @@ new sst.cloudflare.x.SolidStart("Console", {
bucket, bucket,
bucketNew, bucketNew,
database, database,
SECRET.UpstashRedisRestUrl,
SECRET.UpstashRedisRestToken,
AUTH_API_URL, AUTH_API_URL,
STRIPE_WEBHOOK_SECRET, STRIPE_WEBHOOK_SECRET,
DISCORD_INCIDENT_WEBHOOK_URL, DISCORD_INCIDENT_WEBHOOK_URL,
@@ -281,7 +285,7 @@ new sst.cloudflare.x.SolidStart("Console", {
}, },
transform: { transform: {
server: { server: {
placement: { region: "aws:us-east-1" }, placement: { region: "aws:us-east-2" },
transform: { transform: {
worker: { worker: {
tailConsumers: [{ service: logProcessor.nodes.worker.scriptName }], tailConsumers: [{ service: logProcessor.nodes.worker.scriptName }],
+322
View File
@@ -0,0 +1,322 @@
import { domain } from "./stage"
const current = aws.getCallerIdentityOutput({})
const partition = aws.getPartitionOutput({})
const region = aws.getRegionOutput({})
const tableBucketName = `opencode-${$app.stage}-lake`
const glueCatalogName = "s3tablescatalog"
const glueCatalogArn = $interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:catalog`
const glueS3TablesCatalogArn = $interpolate`${glueCatalogArn}/${glueCatalogName}`
const glueS3TablesChildCatalogArn = $interpolate`${glueS3TablesCatalogArn}/${tableBucketName}`
const glueS3TablesDatabaseWildcardArn = $interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:database/${glueCatalogName}/${tableBucketName}/*`
const glueS3TablesTableWildcardArn = $interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:table/${glueCatalogName}/${tableBucketName}/*/*`
const s3TablesBucketWildcardArn = $interpolate`arn:${partition.partition}:s3tables:${region.region}:${current.accountId}:bucket/*`
export const tableBucket = new aws.s3tables.TableBucket("LakeTableBucket", {
name: tableBucketName,
forceDestroy: $app.stage !== "production",
})
const s3TablesCatalog = new aws.cloudcontrol.Resource(
"LakeS3TablesCatalog",
{
typeName: "AWS::Glue::Catalog",
desiredState: $jsonStringify({
Name: glueCatalogName,
Description: "Federated catalog for S3 Tables",
FederatedCatalog: {
Identifier: s3TablesBucketWildcardArn,
ConnectionName: "aws:s3tables",
},
CreateDatabaseDefaultPermissions: [
{
Principal: {
DataLakePrincipalIdentifier: "IAM_ALLOWED_PRINCIPALS",
},
Permissions: ["ALL"],
},
],
CreateTableDefaultPermissions: [
{
Principal: {
DataLakePrincipalIdentifier: "IAM_ALLOWED_PRINCIPALS",
},
Permissions: ["ALL"],
},
],
AllowFullTableExternalDataAccess: "True",
}),
},
{ dependsOn: [tableBucket] },
)
const athenaResultsBucket = new aws.s3.Bucket("LakeAthenaResults", {
bucket: `opencode-${$app.stage}-lake-athena-results`,
forceDestroy: $app.stage !== "production",
})
const firehoseErrorBucket = new aws.s3.Bucket("LakeFirehoseErrors", {
bucket: `opencode-${$app.stage}-lake-firehose-errors`,
forceDestroy: $app.stage !== "production",
})
const athenaWorkgroup = new aws.athena.Workgroup("LakeAthenaWorkgroup", {
name: `opencode-${$app.stage}-lake-workgroup`,
forceDestroy: $app.stage !== "production",
configuration: {
enforceWorkgroupConfiguration: true,
publishCloudwatchMetricsEnabled: true,
resultConfiguration: {
outputLocation: $interpolate`s3://${athenaResultsBucket.bucket}/`,
},
},
})
const firehoseRole = new aws.iam.Role("LakeFirehoseRole", {
assumeRolePolicy: aws.iam.getPolicyDocumentOutput({
statements: [
{
effect: "Allow",
actions: ["sts:AssumeRole"],
principals: [
{
type: "Service",
identifiers: ["firehose.amazonaws.com"],
},
],
},
],
}).json,
})
const firehosePolicy = new aws.iam.RolePolicy("LakeFirehosePolicy", {
role: firehoseRole.id,
policy: aws.iam.getPolicyDocumentOutput({
statements: [
{
effect: "Allow",
actions: [
"s3tables:ListTableBuckets",
"s3tables:GetTableBucket",
"s3tables:GetNamespace",
"s3tables:GetTable",
"s3tables:GetTableData",
"s3tables:GetTableMetadataLocation",
"s3tables:ListNamespaces",
"s3tables:ListTables",
"s3tables:PutTableData",
"s3tables:UpdateTableMetadataLocation",
],
resources: ["*"],
},
{
effect: "Allow",
actions: [
"glue:GetCatalog",
"glue:GetCatalogs",
"glue:GetDatabase",
"glue:GetDatabases",
"glue:GetTable",
"glue:GetTables",
"glue:UpdateTable",
],
resources: [
glueCatalogArn,
glueS3TablesCatalogArn,
$interpolate`${glueS3TablesCatalogArn}/*`,
glueS3TablesDatabaseWildcardArn,
glueS3TablesTableWildcardArn,
$interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:database/*`,
$interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:table/*/*`,
$interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:table/${glueCatalogName}/*`,
],
},
{
effect: "Allow",
actions: [
"s3:AbortMultipartUpload",
"s3:GetBucketLocation",
"s3:GetObject",
"s3:ListBucket",
"s3:ListBucketMultipartUploads",
"s3:PutObject",
],
resources: [firehoseErrorBucket.arn, $interpolate`${firehoseErrorBucket.arn}/*`],
},
{
effect: "Allow",
actions: ["lakeformation:GetDataAccess"],
resources: ["*"],
},
],
}).json,
})
const firehose = new aws.kinesis.FirehoseDeliveryStream(
"LakeFirehose",
{
name: `opencode-${$app.stage}-lake-ingest`,
destination: "iceberg",
icebergConfiguration: {
appendOnly: true,
bufferingInterval: 60,
bufferingSize: 1,
catalogArn: glueS3TablesChildCatalogArn,
processingConfiguration: {
enabled: true,
processors: [
{
type: "MetadataExtraction",
parameters: [
{ parameterName: "JsonParsingEngine", parameterValue: "JQ-1.6" },
{
parameterName: "MetadataExtractionQuery",
parameterValue:
'{destinationDatabaseName:._lake_database,destinationTableName:._lake_table,operation:(._lake_operation // "insert")}',
},
],
},
],
},
roleArn: firehoseRole.arn,
s3BackupMode: "FailedDataOnly",
s3Configuration: {
roleArn: firehoseRole.arn,
bucketArn: firehoseErrorBucket.arn,
errorOutputPrefix: "errors/!{firehose:error-output-type}/",
},
},
},
{ dependsOn: [s3TablesCatalog, firehosePolicy] },
)
export const lakeVpc = new sst.aws.Vpc("LakeVpc")
export const lakeCluster = new sst.aws.Cluster("LakeCluster", { vpc: lakeVpc })
export const lakeRegion = region.region
export const lakeCatalog = $interpolate`${glueCatalogName}/${tableBucket.name}`
export const lakeAthenaWorkgroup = athenaWorkgroup
const ingestSecret = new random.RandomPassword("LakeIngestSecret", { length: 32 })
const ingestConfig = new sst.Linkable("LakeIngestConfig", {
properties: {
streamName: firehose.name,
secret: ingestSecret.result,
},
})
const ingestService = new sst.aws.Service("LakeIngestService", {
cluster: lakeCluster,
architecture: "arm64",
cpu: "1 vCPU",
memory: "4 GB",
image: {
context: ".",
dockerfile: "packages/stats/server/Dockerfile",
},
link: [ingestConfig],
permissions: [
{
actions: ["firehose:PutRecord", "firehose:PutRecordBatch"],
resources: [firehose.arn],
},
],
scaling: {
min: $app.stage === "production" ? 2 : 1,
max: $app.stage === "production" ? 32 : 4,
cpuUtilization: 60,
memoryUtilization: 70,
},
loadBalancer: {
domain: {
name: `lake.${domain}`,
dns: sst.cloudflare.dns(),
},
rules: [
{ listen: "80/http", redirect: "443/https" },
{ listen: "443/https", forward: "3000/http" },
],
health: {
"3000/http": {
path: "/ready",
successCodes: "200-299",
},
},
},
health: {
command: [
"CMD-SHELL",
"bun --eval \"fetch('http://localhost:3000/health').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))\"",
],
interval: "30 seconds",
retries: 3,
startPeriod: "30 seconds",
timeout: "5 seconds",
},
dev: {
command: "bun run start",
directory: "packages/stats/server",
url: "http://localhost:3000",
},
wait: $app.stage === "production",
})
export const lakeIngest = new sst.Linkable("LakeIngest", {
properties: {
url: ingestService.url,
secret: ingestSecret.result,
},
})
export const lakeQueryPermissions = [
{
actions: ["athena:StartQueryExecution", "athena:GetQueryExecution", "athena:GetQueryResults"],
resources: [athenaWorkgroup.arn],
},
{
actions: [
"glue:GetCatalog",
"glue:GetCatalogs",
"glue:GetDatabase",
"glue:GetDatabases",
"glue:GetTable",
"glue:GetTables",
"glue:GetPartitions",
],
resources: [
glueCatalogArn,
glueS3TablesCatalogArn,
$interpolate`${glueS3TablesCatalogArn}/*`,
glueS3TablesDatabaseWildcardArn,
glueS3TablesTableWildcardArn,
$interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:database/*`,
$interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:table/*/*`,
$interpolate`arn:${partition.partition}:glue:${region.region}:${current.accountId}:table/${glueCatalogName}/*`,
],
},
{
actions: ["s3:GetBucketLocation", "s3:ListBucket"],
resources: [athenaResultsBucket.arn],
},
{
actions: ["s3:GetObject", "s3:PutObject", "s3:AbortMultipartUpload", "s3:ListBucketMultipartUploads"],
resources: [$interpolate`${athenaResultsBucket.arn}/*`],
},
{
actions: [
"s3tables:GetTableBucket",
"s3tables:GetNamespace",
"s3tables:GetTable",
"s3tables:GetTableData",
"s3tables:GetTableMetadataLocation",
"s3tables:ListNamespaces",
"s3tables:ListTables",
],
resources: ["*"],
},
{
actions: ["lakeformation:GetDataAccess"],
resources: ["*"],
},
]
+3
View File
@@ -7,5 +7,8 @@ sst.Linkable.wrap(random.RandomPassword, (resource) => ({
export const SECRET = { export const SECRET = {
R2AccessKey: new sst.Secret("R2AccessKey", "unknown"), R2AccessKey: new sst.Secret("R2AccessKey", "unknown"),
R2SecretKey: new sst.Secret("R2SecretKey", "unknown"), R2SecretKey: new sst.Secret("R2SecretKey", "unknown"),
HoneycombApiKey: new sst.Secret("HONEYCOMB_API_KEY"),
HoneycombWebhookSecret: new random.RandomPassword("HoneycombWebhookSecret", { length: 24 }), HoneycombWebhookSecret: new random.RandomPassword("HoneycombWebhookSecret", { length: 24 }),
UpstashRedisRestUrl: new sst.Secret("UpstashRedisRestUrl"),
UpstashRedisRestToken: new sst.Secret("UpstashRedisRestToken"),
} }
+2
View File
@@ -5,6 +5,8 @@ export const domain = (() => {
})() })()
export const zoneID = "430ba34c138cfb5360826c4909f99be8" export const zoneID = "430ba34c138cfb5360826c4909f99be8"
export const awsStage = $app.stage === "production" ? "production" : "dev"
export const deployAws = $app.stage === awsStage
new cloudflare.RegionalHostname("RegionalHostname", { new cloudflare.RegionalHostname("RegionalHostname", {
hostname: domain, hostname: domain,
+207
View File
@@ -0,0 +1,207 @@
import { lakeAthenaWorkgroup, lakeCatalog, lakeCluster, lakeQueryPermissions, lakeRegion, tableBucket } from "./lake"
const domain = (() => {
if ($app.stage === "production") return "stats.opencode.ai"
if ($app.stage === "dev") return "stats.dev.opencode.ai"
return `stats.${$app.stage}.dev.opencode.ai`
})()
////////////////
// LAKE
////////////////
const inferenceNamespace = new aws.s3tables.Namespace("LakeInferenceNamespace", {
namespace: "inference",
tableBucketArn: tableBucket.arn,
})
const inferenceEventTable = new aws.s3tables.Table(
"LakeInferenceEventTable",
{
name: "event",
namespace: inferenceNamespace.namespace,
tableBucketArn: inferenceNamespace.tableBucketArn,
format: "ICEBERG",
metadata: {
iceberg: {
schema: {
fields: [
{ name: "event_timestamp", type: "string", required: false },
{ name: "event_date", type: "string", required: false },
{ name: "event_type", type: "string", required: false },
{ name: "dataset", type: "string", required: false },
{ name: "cf_continent", type: "string", required: false },
{ name: "cf_country", type: "string", required: false },
{ name: "cf_city", type: "string", required: false },
{ name: "cf_region", type: "string", required: false },
{ name: "cf_latitude", type: "double", required: false },
{ name: "cf_longitude", type: "double", required: false },
{ name: "cf_timezone", type: "string", required: false },
{ name: "duration", type: "double", required: false },
{ name: "request_length", type: "long", required: false },
{ name: "status", type: "int", required: false },
{ name: "ip", type: "string", required: false },
{ name: "is_stream", type: "boolean", required: false },
{ name: "session", type: "string", required: false },
{ name: "request", type: "string", required: false },
{ name: "client", type: "string", required: false },
{ name: "user_agent", type: "string", required: false },
{ name: "model_variant", type: "string", required: false },
{ name: "source", type: "string", required: false },
{ name: "provider", type: "string", required: false },
{ name: "provider_model", type: "string", required: false },
{ name: "model", type: "string", required: false },
{ name: "llm_error_code", type: "int", required: false },
{ name: "llm_error_message", type: "string", required: false },
{ name: "error_response", type: "string", required: false },
{ name: "error_type", type: "string", required: false },
{ name: "error_message", type: "string", required: false },
{ name: "error_cause", type: "string", required: false },
{ name: "error_cause2", type: "string", required: false },
{ name: "api_key", type: "string", required: false },
{ name: "workspace", type: "string", required: false },
{ name: "is_subscription", type: "boolean", required: false },
{ name: "subscription", type: "string", required: false },
{ name: "response_length", type: "long", required: false },
{ name: "time_to_first_byte", type: "long", required: false },
{ name: "timestamp_first_byte", type: "long", required: false },
{ name: "timestamp_last_byte", type: "long", required: false },
{ name: "tokens_input", type: "long", required: false },
{ name: "tokens_output", type: "long", required: false },
{ name: "tokens_reasoning", type: "long", required: false },
{ name: "tokens_cache_read", type: "long", required: false },
{ name: "tokens_cache_write_5m", type: "long", required: false },
{ name: "tokens_cache_write_1h", type: "long", required: false },
{ name: "cost_input_microcents", type: "long", required: false },
{ name: "cost_output_microcents", type: "long", required: false },
{ name: "cost_cache_read_microcents", type: "long", required: false },
{ name: "cost_cache_write_microcents", type: "long", required: false },
{ name: "cost_total_microcents", type: "long", required: false },
{ name: "cost_input", type: "long", required: false },
{ name: "cost_output", type: "long", required: false },
{ name: "cost_cache_read", type: "long", required: false },
{ name: "cost_cache_write_5m", type: "long", required: false },
{ name: "cost_cache_write_1h", type: "long", required: false },
{ name: "cost_total", type: "long", required: false },
],
},
},
},
},
{ deleteBeforeReplace: $app.stage !== "production" },
)
export const inferenceEvent = new sst.Linkable("InferenceEvent", {
properties: {
region: lakeRegion,
catalog: lakeCatalog,
database: inferenceNamespace.namespace,
table: inferenceEventTable.name,
tableBucket: tableBucket.name,
workgroup: lakeAthenaWorkgroup.name,
},
})
////////////////
// DATABASE
////////////////
const cluster = planetscale.getDatabaseOutput({
name: "opencode-stats",
organization: "anomalyco",
})
const branch =
$app.stage === "production"
? planetscale.getBranchOutput({
name: "production",
organization: cluster.organization,
database: cluster.name,
})
: new planetscale.Branch("StatsDatabaseBranch", {
database: cluster.name,
organization: cluster.organization,
name: $app.stage,
parentBranch: "production",
})
const password = new planetscale.Password("StatsDatabasePassword", {
name: $app.stage,
database: cluster.name,
organization: cluster.organization,
branch: branch.name,
})
const databaseUrl = $interpolate`mysql://${password.username.apply(encodeURIComponent)}:${password.plaintext.apply(
encodeURIComponent,
)}@${password.accessHostUrl}/${cluster.name}`
export const database = new sst.Linkable("StatsDatabase", {
properties: {
host: password.accessHostUrl,
database: cluster.name,
username: password.username,
password: password.plaintext,
port: 3306,
url: databaseUrl,
},
})
new sst.x.DevCommand("StatsStudio", {
link: [database],
environment: {
DATABASE_URL: databaseUrl,
},
dev: {
command: "bun db:studio",
directory: "packages/stats/core",
autostart: false,
},
})
////////////////
// APP
////////////////
// export const app = new sst.cloudflare.x.SolidStart("Stats", {
// path: "packages/stats/app",
// buildCommand: "bun run build",
// domain,
// link: [database],
// environment: {
// PUBLIC_URL: `https://${domain}`,
// },
// })
////////////////
// SERVICES
////////////////
const statsSyncConfig = new sst.Linkable("StatsSyncConfig", {
properties: {
dataset: "zen",
},
})
export const statSync = new sst.aws.Service("StatsSyncService", {
cluster: lakeCluster,
architecture: "arm64",
cpu: "0.25 vCPU",
memory: "0.5 GB",
image: {
context: ".",
dockerfile: "packages/stats/server/Dockerfile",
},
command: ["bun", "src/stat-sync.ts"],
link: [database, inferenceEvent, statsSyncConfig],
permissions: lakeQueryPermissions,
scaling: {
min: 1,
max: 1,
},
dev: {
command: "bun src/stat-sync.ts",
directory: "packages/stats/server",
autostart: false,
},
})
+10 -1
View File
@@ -3,6 +3,7 @@
stdenv, stdenv,
bun, bun,
nodejs, nodejs,
darwin,
electron_41, electron_41,
makeWrapper, makeWrapper,
writableTmpDirAsHomeHook, writableTmpDirAsHomeHook,
@@ -14,7 +15,12 @@ let
in in
stdenv.mkDerivation (finalAttrs: { stdenv.mkDerivation (finalAttrs: {
pname = "opencode-desktop"; pname = "opencode-desktop";
inherit (opencode) version src node_modules; inherit (opencode)
version
src
node_modules
patches
;
nativeBuildInputs = [ nativeBuildInputs = [
bun bun
@@ -23,6 +29,9 @@ stdenv.mkDerivation (finalAttrs: {
writableTmpDirAsHomeHook writableTmpDirAsHomeHook
] ++ lib.optionals stdenv.hostPlatform.isLinux [ ] ++ lib.optionals stdenv.hostPlatform.isLinux [
autoPatchelfHook autoPatchelfHook
] ++ lib.optionals stdenv.hostPlatform.isDarwin [
# Ad-hoc sign the .app: --config.mac.identity=null below skips signing.
darwin.autoSignDarwinBinariesHook
]; ];
buildInputs = lib.optionals stdenv.hostPlatform.isLinux [ buildInputs = lib.optionals stdenv.hostPlatform.isLinux [
+4 -4
View File
@@ -1,8 +1,8 @@
{ {
"nodeModules": { "nodeModules": {
"x86_64-linux": "sha256-1RiaZQHzIhdtcOJUMsLagpP+nBBL/Qu6zQgrAXMHDCI=", "x86_64-linux": "sha256-70om2TEJYJJOcf9lqex+Sxs27c1SVVgN7FNkLbTXBR0=",
"aarch64-linux": "sha256-5QZhtkWuNpY/qUxlKRHcGbILOAVnuyzu+h8VDIuMQcU=", "aarch64-linux": "sha256-/xTsjAFl6TmydJFXETctBoRePIFAkMyxv0HLWHFymJg=",
"aarch64-darwin": "sha256-9w3QA22XNc1itGLyhikYU90xGH/iLUUM+SSGXla7lhw=", "aarch64-darwin": "sha256-/xbFzrnScm1x4xuz2rEJyyjockDMIIyqJtxm3Axghbg=",
"x86_64-darwin": "sha256-cSYiyhhSqIYiTeK1uWHDkHbYstQYD5jEB7JaYjWjgi4=" "x86_64-darwin": "sha256-ZbJDPhmcvBlY80CPmzygw4S8mjdR+d/UyL+3R3nhZtM="
} }
} }
+11 -6
View File
@@ -10,6 +10,7 @@
"dev:desktop": "bun --cwd packages/desktop dev", "dev:desktop": "bun --cwd packages/desktop dev",
"dev:web": "bun --cwd packages/app dev", "dev:web": "bun --cwd packages/app dev",
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev", "dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
"dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev",
"dev:storybook": "bun --cwd packages/storybook storybook", "dev:storybook": "bun --cwd packages/storybook storybook",
"lint": "oxlint", "lint": "oxlint",
"typecheck": "bun turbo typecheck", "typecheck": "bun turbo typecheck",
@@ -17,13 +18,14 @@
"postinstall": "bun run --cwd packages/opencode fix-node-pty", "postinstall": "bun run --cwd packages/opencode fix-node-pty",
"prepare": "husky", "prepare": "husky",
"random": "echo 'Random script'", "random": "echo 'Random script'",
"hello": "echo 'Hello World!'", "sso": "aws sso login --sso-session=opencode --no-browser",
"test": "echo 'do not run tests from root' && exit 1" "test": "echo 'do not run tests from root' && exit 1"
}, },
"workspaces": { "workspaces": {
"packages": [ "packages": [
"packages/*", "packages/*",
"packages/console/*", "packages/console/*",
"packages/stats/*",
"packages/sdk/js", "packages/sdk/js",
"packages/slack" "packages/slack"
], ],
@@ -36,9 +38,9 @@
"@types/cross-spawn": "6.0.6", "@types/cross-spawn": "6.0.6",
"@octokit/rest": "22.0.0", "@octokit/rest": "22.0.0",
"@hono/zod-validator": "0.4.2", "@hono/zod-validator": "0.4.2",
"@opentui/core": "0.2.15", "@opentui/core": "0.2.16",
"@opentui/keymap": "0.2.15", "@opentui/keymap": "0.2.16",
"@opentui/solid": "0.2.15", "@opentui/solid": "0.2.16",
"ulid": "3.0.1", "ulid": "3.0.1",
"@kobalte/core": "0.13.11", "@kobalte/core": "0.13.11",
"@types/luxon": "3.7.1", "@types/luxon": "3.7.1",
@@ -72,6 +74,7 @@
"@typescript/native-preview": "7.0.0-dev.20251207.1", "@typescript/native-preview": "7.0.0-dev.20251207.1",
"zod": "4.1.8", "zod": "4.1.8",
"remeda": "2.26.0", "remeda": "2.26.0",
"sst": "4.13.1",
"shiki": "3.20.0", "shiki": "3.20.0",
"solid-list": "0.3.0", "solid-list": "0.3.0",
"tailwindcss": "4.1.11", "tailwindcss": "4.1.11",
@@ -98,7 +101,7 @@
"oxlint-tsgolint": "0.21.0", "oxlint-tsgolint": "0.21.0",
"prettier": "3.6.2", "prettier": "3.6.2",
"semver": "^7.6.0", "semver": "^7.6.0",
"sst": "4.13.1", "sst": "catalog:",
"turbo": "2.8.13" "turbo": "2.8.13"
}, },
"dependencies": { "dependencies": {
@@ -140,6 +143,8 @@
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.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", "@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", "solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
"@ai-sdk/xai@3.0.82": "patches/@ai-sdk%2Fxai@3.0.82.patch" "virtua@0.49.1": "patches/virtua@0.49.1.patch",
"@ai-sdk/xai@3.0.82": "patches/@ai-sdk%2Fxai@3.0.82.patch",
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch"
} }
} }
@@ -0,0 +1,352 @@
import { expect, test, type Locator, type Page } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
const directory = "C:/OpenCode/TimelineStateRegression"
const projectID = "proj_timeline_state_regression"
const sessionID = "ses_timeline_state_regression"
const userMessageID = "msg_user_regression"
const assistantMessageID = "msg_assistant_regression"
const editPartID = "prt_0001_edit"
const textPartID = "prt_9999_text"
const title = "Timeline collapse state regression"
const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" }
type EventPayload = {
directory: string
payload: Record<string, unknown>
}
declare global {
interface Window {
__timelineDiffProbe: {
reset: () => void
shadowRoots: () => number
}
}
}
const userMessage = {
info: {
id: userMessageID,
sessionID,
role: "user",
time: { created: 1700000000000 },
summary: { diffs: [] },
agent: "build",
model,
},
parts: [
{
id: "prt_user_text",
sessionID,
messageID: userMessageID,
type: "text",
text: "Please edit the file.",
},
],
}
const editPart = {
id: editPartID,
sessionID,
messageID: assistantMessageID,
type: "tool",
callID: "call_edit_regression",
tool: "edit",
state: {
status: "completed",
input: { filePath: "src/regression.ts" },
output: "Edited src/regression.ts",
title: "src/regression.ts",
metadata: {
filediff: {
file: "src/regression.ts",
additions: 1,
deletions: 1,
before: "export const value = 'before'\n",
after: "export const value = 'after'\n",
},
diff: "diff --git a/src/regression.ts b/src/regression.ts\n-export const value = 'before'\n+export const value = 'after'\n",
},
time: { start: 1700000001000, end: 1700000002000 },
},
}
const streamedTextPart = {
id: textPartID,
sessionID,
messageID: assistantMessageID,
type: "text",
text: "Streaming added a later assistant text part.",
}
const assistantMessage = {
info: {
id: assistantMessageID,
sessionID,
role: "assistant",
time: { created: 1700000001000 },
parentID: userMessageID,
modelID: model.modelID,
providerID: model.providerID,
mode: "build",
agent: "build",
path: { cwd: directory, root: directory },
cost: 0.01,
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
variant: "max",
},
parts: [editPart],
}
test.describe("regression: session timeline local row state", () => {
test("keeps a manually collapsed tool collapsed when later assistant content streams", async ({ page }) => {
const events: EventPayload[] = []
await mockServer(page, events)
await configurePage(page)
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
await expect(page.getByRole("heading", { name: title })).toBeVisible()
const wrapper = page.locator(`[data-timeline-part-id="${editPartID}"]`).first()
await expect(wrapper).toBeVisible()
await expectExpanded(wrapper, true)
await wrapper.evaluate((element) => {
;(element as HTMLElement).dataset.regressionMarker = "before-stream"
})
await wrapper.locator('[data-slot="collapsible-trigger"]').first().click()
await expectExpanded(wrapper, false)
events.push({
directory,
payload: {
type: "message.part.updated",
properties: { part: streamedTextPart },
},
})
await expect(page.locator(`[data-timeline-part-id="${textPartID}"]`).first()).toBeVisible({ timeout: 10_000 })
expect(await readToolState(page)).toEqual({
expanded: false,
row: "AssistantPart",
streamedTextVisible: true,
})
})
test("does not remount an edit diff when sibling parts or diff counts update", async ({ page }) => {
const events: EventPayload[] = []
await installDiffProbe(page)
await mockServer(page, events)
await configurePage(page)
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
await expect(page.getByRole("heading", { name: title })).toBeVisible()
const wrapper = page.locator(`[data-timeline-part-id="${editPartID}"]`).first()
await expect(wrapper).toBeVisible()
await expect(wrapper.locator('[data-component="file"][data-mode="diff"]').first()).toBeVisible()
await markDiffProbe(page)
events.push({
directory,
payload: {
type: "message.part.updated",
properties: { part: streamedTextPart },
},
})
await expect(page.locator(`[data-timeline-part-id="${textPartID}"]`).first()).toBeVisible({ timeout: 10_000 })
expect(await readDiffProbe(page)).toEqual({ fileMarker: "before", shadowRoots: 0, toolMarker: "before" })
await markDiffProbe(page)
events.push({
directory,
payload: {
type: "message.part.updated",
properties: { part: editPartWithAdditions(2) },
},
})
await expect(wrapper.locator('[data-slot="diff-changes-additions"]').filter({ hasText: "+2" }).first()).toBeVisible(
{ timeout: 10_000 },
)
expect(await readDiffProbe(page)).toEqual({ fileMarker: "before", shadowRoots: 0, toolMarker: "before" })
})
})
async function configurePage(page: Page) {
await page.addInitScript(() => {
localStorage.setItem(
"settings.v3",
JSON.stringify({
general: {
editToolPartsExpanded: true,
shellToolPartsExpanded: true,
showReasoningSummaries: true,
showSessionProgressBar: true,
},
}),
)
})
}
async function expectExpanded(locator: Locator, expected: boolean) {
await expect.poll(() => locator.evaluate(readExpanded)).toBe(expected)
}
async function readToolState(page: Page) {
return page
.locator(`[data-timeline-part-id="${editPartID}"]`)
.first()
.evaluate(
(element, textPartID) => ({
expanded: (() => {
const trigger = element.querySelector('[data-slot="collapsible-trigger"]')
const aria = trigger?.getAttribute("aria-expanded")
if (aria === "true") return true
if (aria === "false") return false
const root = element.querySelector('[data-component="collapsible"]')
if (root?.hasAttribute("data-expanded")) return true
if (root?.hasAttribute("data-closed")) return false
const content = element.querySelector<HTMLElement>('[data-slot="collapsible-content"]')
return !!content && content.getBoundingClientRect().height > 0
})(),
row: element.closest("[data-timeline-row]")?.getAttribute("data-timeline-row"),
streamedTextVisible: !!document.querySelector(`[data-timeline-part-id="${textPartID}"]`),
}),
textPartID,
)
}
async function installDiffProbe(page: Page) {
await page.addInitScript(() => {
let shadowRootCount = 0
const attachShadow = Element.prototype.attachShadow
Element.prototype.attachShadow = function (init) {
shadowRootCount += 1
return attachShadow.call(this, init)
}
window.__timelineDiffProbe = {
reset: () => {
shadowRootCount = 0
},
shadowRoots: () => shadowRootCount,
}
})
}
async function markDiffProbe(page: Page) {
await page
.locator(`[data-timeline-part-id="${editPartID}"]`)
.first()
.evaluate((element) => {
const tool = element as HTMLElement
const file = tool.querySelector<HTMLElement>('[data-component="file"][data-mode="diff"]')
if (!file) throw new Error("missing edit diff file")
tool.dataset.timelineProbe = "before"
file.dataset.timelineProbe = "before"
window.__timelineDiffProbe.reset()
})
}
async function readDiffProbe(page: Page) {
return page
.locator(`[data-timeline-part-id="${editPartID}"]`)
.first()
.evaluate((element) => {
const tool = element as HTMLElement
const file = tool.querySelector<HTMLElement>('[data-component="file"][data-mode="diff"]')
return {
fileMarker: file?.dataset.timelineProbe,
shadowRoots: window.__timelineDiffProbe.shadowRoots(),
toolMarker: tool.dataset.timelineProbe,
}
})
}
function editPartWithAdditions(additions: number) {
return {
...editPart,
state: {
...editPart.state,
metadata: {
...editPart.state.metadata,
filediff: {
...editPart.state.metadata.filediff,
additions,
},
},
},
}
}
function readExpanded(element: Element) {
const trigger = element.querySelector('[data-slot="collapsible-trigger"]')
const aria = trigger?.getAttribute("aria-expanded")
if (aria === "true") return true
if (aria === "false") return false
const root = element.querySelector('[data-component="collapsible"]')
if (root?.hasAttribute("data-expanded")) return true
if (root?.hasAttribute("data-closed")) return false
const content = element.querySelector<HTMLElement>('[data-slot="collapsible-content"]')
return !!content && content.getBoundingClientRect().height > 0
}
async function mockServer(page: Page, events: EventPayload[]) {
await mockOpenCodeServer(page, {
directory,
project: project(),
provider: provider(),
sessions: [session()],
pageMessages: () => ({ items: [userMessage, assistantMessage] }),
events: () => events.splice(0),
})
}
function project() {
return {
id: projectID,
worktree: directory,
vcs: "git",
name: "timeline-state-regression",
time: { created: 1700000000000, updated: 1700000000000 },
sandboxes: [],
}
}
function session() {
return {
id: sessionID,
slug: "timeline-state-regression",
projectID,
directory,
title,
version: "dev",
time: { created: 1700000000000, updated: 1700000000000 },
}
}
function provider() {
return {
all: [
{
id: "opencode",
name: "OpenCode",
models: { "claude-opus-4-6": { id: "claude-opus-4-6", name: "Claude Opus 4.6", limit: { context: 200_000 } } },
},
],
connected: ["opencode"],
default: { providerID: "opencode", modelID: "claude-opus-4-6" },
}
}
function base64Encode(value: string) {
return Buffer.from(value, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
}
@@ -0,0 +1,267 @@
import { expect, test, type Page } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
const directory = "C:/OpenCode/ContextResizeRegression"
const projectID = "proj_context_resize_regression"
const sessionID = "ses_context_resize_regression"
const title = "Context resize regression"
const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" }
const contextIDs = ["prt_0100_read", "prt_0101_glob", "prt_0102_grep", "prt_0103_list"]
const followingTextID = "prt_0104_text"
type Message = {
info: Record<string, unknown> & { id: string; role: "user" | "assistant" }
parts: Record<string, unknown>[]
}
const messages = [...Array.from({ length: 8 }, (_, index) => turn(index, false)).flat(), ...turn(10, true)]
test.describe("regression: session timeline context group resize", () => {
test("remeasures a recent explored context group before the next paint", async ({ page }) => {
await page.setViewportSize({ width: 1400, height: 900 })
await mockServer(page)
await configurePage(page)
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
await expect(page.getByRole("heading", { name: title })).toBeVisible()
await expect(page.locator(`[data-timeline-part-ids="${contextIDs.join(",")}"]`).first()).toBeVisible()
await expect(page.locator(`[data-timeline-part-id="${followingTextID}"]`).first()).toBeVisible()
await settle(page)
const samples = await sampleExpansion(page)
const visibleOverlap = samples.filter((sample) => sample.frame >= 1 && sample.overlap > 0.5)
console.log("context resize samples", JSON.stringify(samples, null, 2))
expect(samples[0]?.overlap).toBe(0)
expect(visibleOverlap).toEqual([])
expect(samples.at(-1)?.expanded).toBe("true")
})
})
async function configurePage(page: Page) {
await page.addInitScript(() => {
localStorage.setItem(
"settings.v3",
JSON.stringify({
general: {
editToolPartsExpanded: true,
shellToolPartsExpanded: true,
showReasoningSummaries: true,
showSessionProgressBar: true,
},
}),
)
})
}
async function sampleExpansion(page: Page) {
return page.evaluate(
({ contextIDs, followingTextID }) =>
new Promise<
{
frame: number
label: string
scrollTop: number
scrollHeight: number
contextBottom: number
textTop: number
overlap: number
gap: number
expanded: string | null
}[]
>((resolve) => {
const context = document.querySelector<HTMLElement>(`[data-timeline-part-ids="${contextIDs.join(",")}"]`)
const text = document.querySelector<HTMLElement>(`[data-timeline-part-id="${followingTextID}"]`)
const scroller = context?.closest<HTMLElement>(".scroll-view__viewport")
const trigger = context?.querySelector<HTMLElement>('[data-slot="collapsible-trigger"]')
const contextRow = context?.closest<HTMLElement>('[data-timeline-row="AssistantPart"]')
const textRow = text?.closest<HTMLElement>('[data-timeline-row="AssistantPart"]')
if (!context || !text || !scroller || !trigger || !contextRow || !textRow)
throw new Error("missing regression nodes")
scroller.scrollTop = scroller.scrollHeight
const samples: {
frame: number
label: string
scrollTop: number
scrollHeight: number
contextBottom: number
textTop: number
overlap: number
gap: number
expanded: string | null
}[] = []
const capture = (frame: number, label: string) => {
const contextRect = contextRow.getBoundingClientRect()
const textRect = textRow.getBoundingClientRect()
samples.push({
frame,
label,
scrollTop: Math.round(scroller.scrollTop * 10) / 10,
scrollHeight: Math.round(scroller.scrollHeight * 10) / 10,
contextBottom: Math.round(contextRect.bottom * 10) / 10,
textTop: Math.round(textRect.top * 10) / 10,
overlap: Math.max(0, Math.round((contextRect.bottom - textRect.top) * 10) / 10),
gap: Math.max(0, Math.round((textRect.top - contextRect.bottom) * 10) / 10),
expanded: trigger.getAttribute("aria-expanded"),
})
}
capture(-1, "before")
trigger.click()
capture(0, "sync-after-click")
let frame = 1
const tick = () => {
capture(frame, "raf")
frame += 1
if (frame > 8) {
resolve(samples)
return
}
requestAnimationFrame(tick)
}
requestAnimationFrame(tick)
}),
{ contextIDs, followingTextID },
)
}
function turn(index: number, target: boolean): Message[] {
const userID = id("msg_user", index)
const assistantID = id("msg_assistant", index)
return [
{
info: {
id: userID,
sessionID,
role: "user",
time: { created: 1700000000000 + index * 10_000 },
summary: { diffs: [] },
agent: "build",
model,
},
parts: [{ id: id("prt_user", index), sessionID, messageID: userID, type: "text", text: `User message ${index}` }],
},
{
info: {
id: assistantID,
sessionID,
role: "assistant",
time: { created: 1700000000000 + index * 10_000 + 1_000, completed: 1700000000000 + index * 10_000 + 2_000 },
parentID: userID,
modelID: model.modelID,
providerID: model.providerID,
mode: "build",
agent: "build",
path: { cwd: directory, root: directory },
cost: 0.01,
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
variant: "max",
finish: "stop",
},
parts: target
? [
contextTool(contextIDs[0]!, assistantID, "read", { filePath: "src/recent-a.ts", offset: 0, limit: 120 }),
contextTool(contextIDs[1]!, assistantID, "glob", { path: directory, pattern: "**/*.ts" }),
contextTool(contextIDs[2]!, assistantID, "grep", { path: directory, pattern: "Explored", include: "*.ts" }),
contextTool(contextIDs[3]!, assistantID, "list", { path: "src" }),
{
id: followingTextID,
sessionID,
messageID: assistantID,
type: "text",
text: "This assistant text is immediately after the explored context group.",
},
]
: [
{
id: id("prt_text", index),
sessionID,
messageID: assistantID,
type: "text",
text: `Assistant filler ${index}. ${"filler ".repeat(60)}`,
},
],
},
]
}
function contextTool(partID: string, messageID: string, tool: string, input: Record<string, unknown>) {
return {
id: partID,
sessionID,
messageID,
type: "tool",
callID: `call_${partID}`,
tool,
state: {
status: "completed",
input,
output: `Completed ${tool}.\n${"detail line\n".repeat(8)}`,
title: input.filePath || input.path || input.pattern || "completed",
metadata: {},
time: { start: 1700000000000, end: 1700000000100 },
},
}
}
async function mockServer(page: Page) {
await mockOpenCodeServer(page, {
directory,
project: project(),
provider: provider(),
sessions: [session()],
pageMessages: () => ({ items: messages }),
})
}
async function settle(page: Page) {
await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))))
}
function id(prefix: string, index: number) {
return `${prefix}_${String(index).padStart(4, "0")}`
}
function project() {
return {
id: projectID,
worktree: directory,
vcs: "git",
name: "context-resize-regression",
time: { created: 1700000000000, updated: 1700000000000 },
sandboxes: [],
}
}
function session() {
return {
id: sessionID,
slug: "context-resize-regression",
projectID,
directory,
title,
version: "dev",
time: { created: 1700000000000, updated: 1700000000000 },
}
}
function provider() {
return {
all: [
{
id: "opencode",
name: "OpenCode",
models: { "claude-opus-4-6": { id: "claude-opus-4-6", name: "Claude Opus 4.6", limit: { context: 200_000 } } },
},
],
connected: ["opencode"],
default: { providerID: "opencode", modelID: "claude-opus-4-6" },
}
}
function base64Encode(value: string) {
return Buffer.from(value, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
}
+9 -3
View File
@@ -18,6 +18,7 @@ export interface MockServerConfig {
project: unknown project: unknown
sessions: ({ id: string } & Record<string, unknown>)[] sessions: ({ id: string } & Record<string, unknown>)[]
pageMessages: (sessionId: string, limit: number, before?: string) => { items: unknown[]; cursor?: string } pageMessages: (sessionId: string, limit: number, before?: string) => { items: unknown[]; cursor?: string }
events?: () => unknown[]
} }
export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
@@ -43,7 +44,8 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
if (url.port !== targetPort) return route.fallback() if (url.port !== targetPort) return route.fallback()
const path = url.pathname const path = url.pathname
if (path === "/global/event" || path === "/event") return sse(route) if (path === "/global/event" || path === "/event") return sse(route, config.events?.())
if (path === "/global/health") return json(route, { healthy: true })
if (emptyObject.has(path)) return json(route, {}) if (emptyObject.has(path)) return json(route, {})
if (emptyList.has(path)) return json(route, []) if (emptyList.has(path)) return json(route, [])
if (path in staticRoutes) return json(route, staticRoutes[path]) if (path in staticRoutes) return json(route, staticRoutes[path])
@@ -81,6 +83,10 @@ function json(route: Route, body: unknown, headers?: Record<string, string>) {
}) })
} }
function sse(route: Route) { function sse(route: Route, events?: unknown[]) {
return route.fulfill({ status: 200, contentType: "text/event-stream", body: ": ok\n\n" }) return route.fulfill({
status: 200,
contentType: "text/event-stream",
body: events?.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("") || ": ok\n\n",
})
} }
+4 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.15.7", "version": "1.15.11",
"description": "", "description": "",
"type": "module", "type": "module",
"exports": { "exports": {
@@ -42,10 +42,10 @@
}, },
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@sentry/solid": "catalog:", "@opencode-ai/core": "workspace:*",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
"@opencode-ai/core": "workspace:*", "@sentry/solid": "catalog:",
"@shikijs/transformers": "3.9.2", "@shikijs/transformers": "3.9.2",
"@solid-primitives/active-element": "2.1.3", "@solid-primitives/active-element": "2.1.3",
"@solid-primitives/audio": "1.4.2", "@solid-primitives/audio": "1.4.2",
@@ -54,6 +54,7 @@
"@solid-primitives/i18n": "2.2.1", "@solid-primitives/i18n": "2.2.1",
"@solid-primitives/media": "2.3.3", "@solid-primitives/media": "2.3.3",
"@solid-primitives/resize-observer": "2.1.5", "@solid-primitives/resize-observer": "2.1.5",
"@solid-primitives/scheduled": "1.5.3",
"@solid-primitives/scroll": "2.1.3", "@solid-primitives/scroll": "2.1.3",
"@solid-primitives/storage": "catalog:", "@solid-primitives/storage": "catalog:",
"@solid-primitives/timer": "1.4.4", "@solid-primitives/timer": "1.4.4",
Binary file not shown.
+53 -41
View File
@@ -14,6 +14,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
import { Effect } from "effect" import { Effect } from "effect"
import { import {
type Component, type Component,
createEffect,
createMemo, createMemo,
createResource, createResource,
createSignal, createSignal,
@@ -30,8 +31,8 @@ import { Dynamic } from "solid-js/web"
import { CommandProvider } from "@/context/command" import { CommandProvider } from "@/context/command"
import { CommentsProvider } from "@/context/comments" import { CommentsProvider } from "@/context/comments"
import { FileProvider } from "@/context/file" import { FileProvider } from "@/context/file"
import { GlobalSDKProvider } from "@/context/global-sdk" import { ServerSDKProvider } from "@/context/server-sdk"
import { GlobalSyncProvider } from "@/context/global-sync" import { ServerSyncProvider } from "@/context/server-sync"
import { HighlightsProvider } from "@/context/highlights" import { HighlightsProvider } from "@/context/highlights"
import { LanguageProvider, type Locale, useLanguage } from "@/context/language" import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
import { LayoutProvider } from "@/context/layout" import { LayoutProvider } from "@/context/layout"
@@ -40,30 +41,26 @@ import { NotificationProvider } from "@/context/notification"
import { PermissionProvider } from "@/context/permission" import { PermissionProvider } from "@/context/permission"
import { PromptProvider } from "@/context/prompt" import { PromptProvider } from "@/context/prompt"
import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server" import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
import { SettingsProvider } from "@/context/settings" import { SettingsProvider, useSettings } from "@/context/settings"
import { TerminalProvider } from "@/context/terminal" import { TerminalProvider } from "@/context/terminal"
import DirectoryLayout from "@/pages/directory-layout" import DirectoryLayout from "@/pages/directory-layout"
import Layout from "@/pages/layout" import Layout from "@/pages/layout"
import { ErrorPage } from "./pages/error" import { ErrorPage } from "./pages/error"
import { useCheckServerHealth } from "./utils/server-health" import { useCheckServerHealth } from "./utils/server-health"
import { ServersProvider } from "./context/servers"
const HomeRoute = lazy(() => import("@/pages/home")) const HomeRoute = lazy(() => import("@/pages/home"))
const loadSession = () => import("@/pages/session") const Session = lazy(() => import("@/pages/session"))
const Session = lazy(loadSession)
const Loading = () => <div class="size-full" />
if (typeof location === "object" && /\/session(?:\/|$)/.test(location.pathname)) { const SessionRoute = Object.assign(
void loadSession() () => (
} <SessionProviders>
<Session />
const SessionRoute = () => ( </SessionProviders>
<SessionProviders> ),
<Session /> { preload: Session.preload },
</SessionProviders>
) )
const SessionIndexRoute = () => <Navigate href="session" />
function UiI18nBridge(props: ParentProps) { function UiI18nBridge(props: ParentProps) {
const language = useLanguage() const language = useLanguage()
return <I18nProvider value={{ locale: language.intl, t: language.t }}>{props.children}</I18nProvider> return <I18nProvider value={{ locale: language.intl, t: language.t }}>{props.children}</I18nProvider>
@@ -96,9 +93,26 @@ function QueryProvider(props: ParentProps) {
return <QueryClientProvider client={client}>{props.children}</QueryClientProvider> return <QueryClientProvider client={client}>{props.children}</QueryClientProvider>
} }
function BodyDesignClass() {
const settings = useSettings()
createEffect(() => {
if (typeof document === "undefined") return
const enabled = settings.general.newLayoutDesigns()
document.body.classList.toggle("text-12-regular", !enabled)
document.body.classList.toggle("font-(family-name:--font-family-text)", enabled)
document.body.classList.toggle("text-[13px]", enabled)
document.body.classList.toggle("font-[440]", enabled)
})
return null
}
function AppShellProviders(props: ParentProps) { function AppShellProviders(props: ParentProps) {
return ( return (
<SettingsProvider> <SettingsProvider>
<BodyDesignClass />
<PermissionProvider> <PermissionProvider>
<LayoutProvider> <LayoutProvider>
<NotificationProvider> <NotificationProvider>
@@ -301,31 +315,29 @@ export function AppInterface(props: {
disableHealthCheck?: boolean disableHealthCheck?: boolean
}) { }) {
return ( return (
<ServerProvider <ServerProvider defaultServer={props.defaultServer} servers={props.servers}>
defaultServer={props.defaultServer} <ServersProvider>
disableHealthCheck={props.disableHealthCheck} <ConnectionGate disableHealthCheck={props.disableHealthCheck}>
servers={props.servers} <ServerKey>
> <QueryProvider>
<ConnectionGate disableHealthCheck={props.disableHealthCheck}> <ServerSDKProvider>
<ServerKey> <ServerSyncProvider>
<QueryProvider> <Dynamic
<GlobalSDKProvider> component={props.router ?? Router}
<GlobalSyncProvider> root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
<Dynamic >
component={props.router ?? Router} <Route path="/" component={HomeRoute} />
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>} <Route path="/:dir" component={DirectoryLayout}>
> <Route path="/" component={() => <Navigate href="session" />} />
<Route path="/" component={HomeRoute} /> <Route path="/session/:id?" component={SessionRoute} />
<Route path="/:dir" component={DirectoryLayout}> </Route>
<Route path="/" component={SessionIndexRoute} /> </Dynamic>
<Route path="/session/:id?" component={SessionRoute} /> </ServerSyncProvider>
</Route> </ServerSDKProvider>
</Dynamic> </QueryProvider>
</GlobalSyncProvider> </ServerKey>
</GlobalSDKProvider> </ConnectionGate>
</QueryProvider> </ServersProvider>
</ServerKey>
</ConnectionGate>
</ServerProvider> </ServerProvider>
) )
} }
@@ -12,15 +12,15 @@ import { showToast } from "@opencode-ai/ui/toast"
import { createEffect, createMemo, createResource, Match, onCleanup, onMount, Switch } from "solid-js" import { createEffect, createMemo, createResource, Match, onCleanup, onMount, Switch } from "solid-js"
import { createStore, produce } from "solid-js/store" import { createStore, produce } from "solid-js/store"
import { Link } from "@/components/link" import { Link } from "@/components/link"
import { useGlobalSDK } from "@/context/global-sdk" import { useServerSDK } from "@/context/server-sdk"
import { useGlobalSync } from "@/context/global-sync" import { useServerSync } from "@/context/server-sync"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useProviders } from "@/hooks/use-providers" import { useProviders } from "@/hooks/use-providers"
export function DialogConnectProvider(props: { provider: string }) { export function DialogConnectProvider(props: { provider: string }) {
const dialog = useDialog() const dialog = useDialog()
const globalSync = useGlobalSync() const serverSync = useServerSync()
const globalSDK = useGlobalSDK() const serverSDK = useServerSDK()
const language = useLanguage() const language = useLanguage()
const providers = useProviders() const providers = useProviders()
@@ -41,9 +41,7 @@ export function DialogConnectProvider(props: { provider: string }) {
}) })
const provider = createMemo( const provider = createMemo(
() => () => providers.all().get(props.provider) ?? serverSync.data.provider.all.get(props.provider)!,
providers.all().find((x) => x.id === props.provider) ??
globalSync.data.provider.all.find((x) => x.id === props.provider)!,
) )
const fallback = createMemo<ProviderAuthMethod[]>(() => [ const fallback = createMemo<ProviderAuthMethod[]>(() => [
{ {
@@ -54,16 +52,16 @@ export function DialogConnectProvider(props: { provider: string }) {
const [auth] = createResource( const [auth] = createResource(
() => props.provider, () => props.provider,
async () => { async () => {
const cached = globalSync.data.provider_auth[props.provider] const cached = serverSync.data.provider_auth[props.provider]
if (cached) return cached if (cached) return cached
const res = await globalSDK.client.provider.auth() const res = await serverSDK.client.provider.auth()
if (!alive.value) return fallback() if (!alive.value) return fallback()
globalSync.set("provider_auth", res.data ?? {}) serverSync.set("provider_auth", res.data ?? {})
return res.data?.[props.provider] ?? fallback() return res.data?.[props.provider] ?? fallback()
}, },
) )
const loading = createMemo(() => auth.loading && !globalSync.data.provider_auth[props.provider]) const loading = createMemo(() => auth.loading && !serverSync.data.provider_auth[props.provider])
const methods = createMemo(() => auth.latest ?? globalSync.data.provider_auth[props.provider] ?? fallback()) const methods = createMemo(() => auth.latest ?? serverSync.data.provider_auth[props.provider] ?? fallback())
const [store, setStore] = createStore({ const [store, setStore] = createStore({
methodIndex: undefined as undefined | number, methodIndex: undefined as undefined | number,
authorization: undefined as undefined | ProviderAuthAuthorization, authorization: undefined as undefined | ProviderAuthAuthorization,
@@ -160,7 +158,7 @@ export function DialogConnectProvider(props: { provider: string }) {
} }
dispatch({ type: "auth.pending" }) dispatch({ type: "auth.pending" })
const start = Date.now() const start = Date.now()
await globalSDK.client.provider.oauth await serverSDK.client.provider.oauth
.authorize( .authorize(
{ {
providerID: props.provider, providerID: props.provider,
@@ -332,7 +330,7 @@ export function DialogConnectProvider(props: { provider: string }) {
}) })
async function complete() { async function complete() {
await globalSDK.client.global.dispose() await serverSDK.client.global.dispose()
dialog.close() dialog.close()
showToast({ showToast({
variant: "success", variant: "success",
@@ -409,7 +407,7 @@ export function DialogConnectProvider(props: { provider: string }) {
} }
setFormStore("error", undefined) setFormStore("error", undefined)
await globalSDK.client.auth.set({ await serverSDK.client.auth.set({
providerID: props.provider, providerID: props.provider,
auth: { auth: {
type: "api", type: "api",
@@ -480,7 +478,7 @@ export function DialogConnectProvider(props: { provider: string }) {
} }
setFormStore("error", undefined) setFormStore("error", undefined)
const result = await globalSDK.client.provider.oauth const result = await serverSDK.client.provider.oauth
.callback({ .callback({
providerID: props.provider, providerID: props.provider,
method: store.methodIndex, method: store.methodIndex,
@@ -526,14 +524,14 @@ export function DialogConnectProvider(props: { provider: string }) {
const code = createMemo(() => { const code = createMemo(() => {
const instructions = store.authorization?.instructions const instructions = store.authorization?.instructions
if (instructions?.includes(":")) { if (instructions?.includes(":")) {
return instructions.split(":")[1]?.trim() return instructions.split(":").pop()?.trim()
} }
return instructions return instructions
}) })
onMount(() => { onMount(() => {
void (async () => { void (async () => {
const result = await globalSDK.client.provider.oauth const result = await serverSDK.client.provider.oauth
.callback({ .callback({
providerID: props.provider, providerID: props.provider,
method: store.methodIndex, method: store.methodIndex,
@@ -9,8 +9,8 @@ import { showToast } from "@opencode-ai/ui/toast"
import { batch, For } from "solid-js" import { batch, For } from "solid-js"
import { createStore, produce } from "solid-js/store" import { createStore, produce } from "solid-js/store"
import { Link } from "@/components/link" import { Link } from "@/components/link"
import { useGlobalSDK } from "@/context/global-sdk" import { useServerSDK } from "@/context/server-sdk"
import { useGlobalSync } from "@/context/global-sync" import { useServerSync } from "@/context/server-sync"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { type FormState, headerRow, modelRow, validateCustomProvider } from "./dialog-custom-provider-form" import { type FormState, headerRow, modelRow, validateCustomProvider } from "./dialog-custom-provider-form"
import { DialogSelectProvider } from "./dialog-select-provider" import { DialogSelectProvider } from "./dialog-select-provider"
@@ -21,8 +21,8 @@ type Props = {
export function DialogCustomProvider(props: Props) { export function DialogCustomProvider(props: Props) {
const dialog = useDialog() const dialog = useDialog()
const globalSync = useGlobalSync() const serverSync = useServerSync()
const globalSDK = useGlobalSDK() const serverSDK = useServerSDK()
const language = useLanguage() const language = useLanguage()
const [form, setForm] = createStore<FormState>({ const [form, setForm] = createStore<FormState>({
@@ -105,8 +105,8 @@ export function DialogCustomProvider(props: Props) {
const output = validateCustomProvider({ const output = validateCustomProvider({
form, form,
t: language.t, t: language.t,
disabledProviders: globalSync.data.config.disabled_providers ?? [], disabledProviders: serverSync.data.config.disabled_providers ?? [],
existingProviderIDs: new Set(globalSync.data.provider.all.map((p) => p.id)), existingProviderIDs: new Set(serverSync.data.provider.all.keys()),
}) })
batch(() => { batch(() => {
setForm("err", output.err) setForm("err", output.err)
@@ -118,11 +118,11 @@ export function DialogCustomProvider(props: Props) {
const saveMutation = useMutation(() => ({ const saveMutation = useMutation(() => ({
mutationFn: async (result: NonNullable<ReturnType<typeof validate>>) => { mutationFn: async (result: NonNullable<ReturnType<typeof validate>>) => {
const disabledProviders = globalSync.data.config.disabled_providers ?? [] const disabledProviders = serverSync.data.config.disabled_providers ?? []
const nextDisabled = disabledProviders.filter((id) => id !== result.providerID) const nextDisabled = disabledProviders.filter((id) => id !== result.providerID)
if (result.key) { if (result.key) {
await globalSDK.client.auth.set({ await serverSDK.client.auth.set({
providerID: result.providerID, providerID: result.providerID,
auth: { auth: {
type: "api", type: "api",
@@ -131,7 +131,7 @@ export function DialogCustomProvider(props: Props) {
}) })
} }
await globalSync.updateConfig({ await serverSync.updateConfig({
provider: { [result.providerID]: result.config }, provider: { [result.providerID]: result.config },
disabled_providers: nextDisabled, disabled_providers: nextDisabled,
}) })
@@ -6,8 +6,8 @@ import { useMutation } from "@tanstack/solid-query"
import { Icon } from "@opencode-ai/ui/icon" import { Icon } from "@opencode-ai/ui/icon"
import { createMemo, For, Show } from "solid-js" import { createMemo, For, Show } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { useGlobalSDK } from "@/context/global-sdk" import { useServerSDK } from "@/context/server-sdk"
import { useGlobalSync } from "@/context/global-sync" import { useServerSync } from "@/context/server-sync"
import { type LocalProject, getAvatarColors } from "@/context/layout" import { type LocalProject, getAvatarColors } from "@/context/layout"
import { getFilename } from "@opencode-ai/core/util/path" import { getFilename } from "@opencode-ai/core/util/path"
import { Avatar } from "@opencode-ai/ui/avatar" import { Avatar } from "@opencode-ai/ui/avatar"
@@ -18,8 +18,8 @@ const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] a
export function DialogEditProject(props: { project: LocalProject }) { export function DialogEditProject(props: { project: LocalProject }) {
const dialog = useDialog() const dialog = useDialog()
const globalSDK = useGlobalSDK() const serverSDK = useServerSDK()
const globalSync = useGlobalSync() const serverSync = useServerSync()
const language = useLanguage() const language = useLanguage()
const folderName = createMemo(() => getFilename(props.project.worktree)) const folderName = createMemo(() => getFilename(props.project.worktree))
@@ -78,19 +78,19 @@ export function DialogEditProject(props: { project: LocalProject }) {
const start = store.startup.trim() const start = store.startup.trim()
if (props.project.id && props.project.id !== "global") { if (props.project.id && props.project.id !== "global") {
await globalSDK.client.project.update({ await serverSDK.client.project.update({
projectID: props.project.id, projectID: props.project.id,
directory: props.project.worktree, directory: props.project.worktree,
name, name,
icon: { color: store.color || "", override: store.iconOverride || "" }, icon: { color: store.color || "", override: store.iconOverride || "" },
commands: { start }, commands: { start },
}) })
globalSync.project.icon(props.project.worktree, store.iconOverride || undefined) serverSync.project.icon(props.project.worktree, store.iconOverride || undefined)
dialog.close() dialog.close()
return return
} }
globalSync.project.meta(props.project.worktree, { serverSync.project.meta(props.project.worktree, {
name, name,
icon: { color: store.color || undefined, override: store.iconOverride || undefined }, icon: { color: store.color || undefined, override: store.iconOverride || undefined },
commands: { start: start || undefined }, commands: { start: start || undefined },
@@ -6,8 +6,8 @@ import type { ListRef } from "@opencode-ai/ui/list"
import { getDirectory, getFilename } from "@opencode-ai/core/util/path" import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
import fuzzysort from "fuzzysort" import fuzzysort from "fuzzysort"
import { createMemo, createResource, createSignal } from "solid-js" import { createMemo, createResource, createSignal } from "solid-js"
import { useGlobalSDK } from "@/context/global-sdk" import { useServerSDK } from "@/context/server-sdk"
import { useGlobalSync } from "@/context/global-sync" import { useServerSync } from "@/context/server-sync"
import { useLayout } from "@/context/layout" import { useLayout } from "@/context/layout"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
@@ -128,7 +128,7 @@ function uniqueRows(rows: Row[]) {
} }
function useDirectorySearch(args: { function useDirectorySearch(args: {
sdk: ReturnType<typeof useGlobalSDK> sdk: ReturnType<typeof useServerSDK>
start: () => string | undefined start: () => string | undefined
home: () => string home: () => string
}) { }) {
@@ -246,8 +246,8 @@ function useDirectorySearch(args: {
} }
export function DialogSelectDirectory(props: DialogSelectDirectoryProps) { export function DialogSelectDirectory(props: DialogSelectDirectoryProps) {
const sync = useGlobalSync() const sync = useServerSync()
const sdk = useGlobalSDK() const sdk = useServerSDK()
const layout = useLayout() const layout = useLayout()
const dialog = useDialog() const dialog = useDialog()
const language = useLanguage() const language = useLanguage()
@@ -9,8 +9,8 @@ import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
import { useNavigate } from "@solidjs/router" import { useNavigate } from "@solidjs/router"
import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js" import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js"
import { formatKeybind, useCommand, type CommandOption } from "@/context/command" import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
import { useGlobalSDK } from "@/context/global-sdk" import { useServerSDK } from "@/context/server-sdk"
import { useGlobalSync } from "@/context/global-sync" import { useServerSync } from "@/context/server-sync"
import { useLayout } from "@/context/layout" import { useLayout } from "@/context/layout"
import { useFile } from "@/context/file" import { useFile } from "@/context/file"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
@@ -175,7 +175,7 @@ function createFileEntries(props: {
function createSessionEntries(props: { function createSessionEntries(props: {
workspaces: () => string[] workspaces: () => string[]
label: (directory: string) => string label: (directory: string) => string
globalSDK: ReturnType<typeof useGlobalSDK> serverSDK: ReturnType<typeof useServerSDK>
language: ReturnType<typeof useLanguage> language: ReturnType<typeof useLanguage>
}) { }) {
const state: { const state: {
@@ -207,7 +207,7 @@ function createSessionEntries(props: {
state.inflight = Promise.all( state.inflight = Promise.all(
dirs.map((directory) => { dirs.map((directory) => {
const description = props.label(directory) const description = props.label(directory)
return props.globalSDK.client.session return props.serverSDK.client.session
.list({ directory, roots: true }) .list({ directory, roots: true })
.then((x) => .then((x) =>
(x.data ?? []) (x.data ?? [])
@@ -268,8 +268,8 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
const file = useFile() const file = useFile()
const dialog = useDialog() const dialog = useDialog()
const navigate = useNavigate() const navigate = useNavigate()
const globalSDK = useGlobalSDK() const serverSDK = useServerSDK()
const globalSync = useGlobalSync() const serverSync = useServerSync()
const { params, tabs, view } = useSessionLayout() const { params, tabs, view } = useSessionLayout()
const filesOnly = () => props.mode === "files" const filesOnly = () => props.mode === "files"
const state = { cleanup: undefined as (() => void) | void, committed: false } const state = { cleanup: undefined as (() => void) | void, committed: false }
@@ -292,21 +292,21 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
if (directory && !dirs.includes(directory)) return [...dirs, directory] if (directory && !dirs.includes(directory)) return [...dirs, directory]
return dirs return dirs
}) })
const homedir = createMemo(() => globalSync.data.path.home) const homedir = createMemo(() => serverSync.data.path.home)
const label = (directory: string) => { const label = (directory: string) => {
const current = project() const current = project()
const kind = const kind =
current && directory === current.worktree current && directory === current.worktree
? language.t("workspace.type.local") ? language.t("workspace.type.local")
: language.t("workspace.type.sandbox") : language.t("workspace.type.sandbox")
const [store] = globalSync.child(directory, { bootstrap: false }) const [store] = serverSync.child(directory, { bootstrap: false })
const home = homedir() const home = homedir()
const path = home ? directory.replace(home, "~") : directory const path = home ? directory.replace(home, "~") : directory
const name = store.vcs?.branch ?? getFilename(directory) const name = store.vcs?.branch ?? getFilename(directory)
return `${kind} : ${name || path}` return `${kind} : ${name || path}`
} }
const { sessions } = createSessionEntries({ workspaces, label, globalSDK, language }) const { sessions } = createSessionEntries({ workspaces, label, serverSDK, language })
const items = async (text: string) => { const items = async (text: string) => {
const query = text.trim() const query = text.trim()
@@ -6,7 +6,7 @@ import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list" import { List } from "@opencode-ai/ui/list"
import { Switch } from "@opencode-ai/ui/switch" import { Switch } from "@opencode-ai/ui/switch"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useQueryOptions } from "@/context/global-sync" import { useQueryOptions } from "@/context/server-sync"
import { pathKey } from "@/utils/path-key" import { pathKey } from "@/utils/path-key"
const statusLabels = { const statusLabels = {
@@ -91,7 +91,7 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props
<div class="w-full"> <div class="w-full">
<List <List
class="w-full px-0" class="w-full px-0"
key={(x) => x?.id} key={(p) => p.id}
items={providers.popular} items={providers.popular}
activeIcon="plus-small" activeIcon="plus-small"
sortBy={(a, b) => { sortBy={(a, b) => {
@@ -35,7 +35,7 @@ export const DialogSelectProvider: Component = () => {
key={(x) => x?.id} key={(x) => x?.id}
items={() => { items={() => {
language.locale() language.locale()
return [{ id: CUSTOM_ID, name: customLabel() }, ...providers.all()] return [{ id: CUSTOM_ID, name: customLabel() }, ...providers.all().values()]
}} }}
filterKeys={["id", "name"]} filterKeys={["id", "name"]}
groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())} groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())}
+489 -229
View File
@@ -4,6 +4,8 @@ import {
createEffect, createEffect,
on, on,
Component, Component,
splitProps,
For,
Show, Show,
onCleanup, onCleanup,
createMemo, createMemo,
@@ -11,7 +13,10 @@ import {
createResource, createResource,
Switch, Switch,
Match, Match,
type ComponentProps,
type JSX,
} from "solid-js" } from "solid-js"
import { Popover as KobaltePopover } from "@kobalte/core/popover"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { useLocal } from "@/context/local" import { useLocal } from "@/context/local"
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file" import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
@@ -26,12 +31,14 @@ import {
FileAttachmentPart, FileAttachmentPart,
} from "@/context/prompt" } from "@/context/prompt"
import { useLayout } from "@/context/layout" import { useLayout } from "@/context/layout"
import { useNavigate } from "@solidjs/router"
import { useSDK } from "@/context/sdk" import { useSDK } from "@/context/sdk"
import { useServer } from "@/context/server"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
import { useComments } from "@/context/comments" import { useComments } from "@/context/comments"
import { Button } from "@opencode-ai/ui/button" import { Button } from "@opencode-ai/ui/button"
import { DockShellForm, DockTray } from "@opencode-ai/ui/dock-surface" import { DockShellForm, DockTray } from "@opencode-ai/ui/dock-surface"
import { Icon } from "@opencode-ai/ui/icon" import { Icon, type IconProps } from "@opencode-ai/ui/icon"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { IconButton } from "@opencode-ai/ui/icon-button" import { IconButton } from "@opencode-ai/ui/icon-button"
@@ -44,6 +51,7 @@ import { Persist, persisted } from "@/utils/persist"
import { usePermission } from "@/context/permission" import { usePermission } from "@/context/permission"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform" import { usePlatform } from "@/context/platform"
import { useSettings } from "@/context/settings"
import { useSessionLayout } from "@/pages/session/session-layout" import { useSessionLayout } from "@/pages/session/session-layout"
import { createSessionTabs } from "@/pages/session/helpers" import { createSessionTabs } from "@/pages/session/helpers"
import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom" import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
@@ -66,16 +74,16 @@ import { PromptDragOverlay } from "./prompt-input/drag-overlay"
import { promptPlaceholder } from "./prompt-input/placeholder" import { promptPlaceholder } from "./prompt-input/placeholder"
import { ImagePreview } from "@opencode-ai/ui/image-preview" import { ImagePreview } from "@opencode-ai/ui/image-preview"
import { useQueries } from "@tanstack/solid-query" import { useQueries } from "@tanstack/solid-query"
import { useQueryOptions } from "@/context/global-sync" import { useQueryOptions } from "@/context/server-sync"
import { pathKey } from "@/utils/path-key" import { pathKey } from "@/utils/path-key"
import { getFilename } from "@opencode-ai/core/util/path" import { base64Encode } from "@opencode-ai/core/util/encode"
import { displayName } from "@/pages/layout/helpers"
interface PromptInputProps { interface PromptInputProps {
class?: string class?: string
variant?: "dock" | "new-session" variant?: "dock" | "new-session"
ref?: (el: HTMLDivElement) => void ref?: (el: HTMLDivElement) => void
newSessionWorktree?: string newSessionWorktree?: string
onNewSessionWorktreeChange?: (worktree: string) => void
onNewSessionWorktreeReset?: () => void onNewSessionWorktreeReset?: () => void
edit?: { id: string; prompt: Prompt; context: FollowupDraft["context"] } edit?: { id: string; prompt: Prompt; context: FollowupDraft["context"] }
onEditLoaded?: () => void onEditLoaded?: () => void
@@ -113,11 +121,9 @@ const EXAMPLES = [
"prompt.example.25", "prompt.example.25",
] as const ] as const
const MAIN_WORKTREE = "main"
const CREATE_WORKTREE = "create"
export const PromptInput: Component<PromptInputProps> = (props) => { export const PromptInput: Component<PromptInputProps> = (props) => {
const sdk = useSDK() const sdk = useSDK()
const navigate = useNavigate()
const queryOptions = useQueryOptions() const queryOptions = useQueryOptions()
const sync = useSync() const sync = useSync()
@@ -125,6 +131,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const files = useFile() const files = useFile()
const prompt = usePrompt() const prompt = usePrompt()
const layout = useLayout() const layout = useLayout()
const server = useServer()
const comments = useComments() const comments = useComments()
const dialog = useDialog() const dialog = useDialog()
const providers = useProviders() const providers = useProviders()
@@ -132,11 +139,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const permission = usePermission() const permission = usePermission()
const language = useLanguage() const language = useLanguage()
const platform = usePlatform() const platform = usePlatform()
const settings = useSettings()
const { params, tabs, view } = useSessionLayout() const { params, tabs, view } = useSessionLayout()
let editorRef!: HTMLDivElement let editorRef!: HTMLDivElement
let fileInputRef: HTMLInputElement | undefined let fileInputRef: HTMLInputElement | undefined
let scrollRef!: HTMLDivElement let scrollRef!: HTMLDivElement
let slashPopoverRef!: HTMLDivElement let slashPopoverRef!: HTMLDivElement
let projectSearchRef: HTMLInputElement | undefined
const mirror = { input: false } const mirror = { input: false }
const inset = 56 const inset = 56
@@ -277,6 +286,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
mode: "normal", mode: "normal",
applyingHistory: false, applyingHistory: false,
}) })
const [picker, setPicker] = createStore({
projectOpen: false,
projectSearch: "",
})
const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 }) const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 })
const motion = (value: number) => ({ const motion = (value: number) => ({
@@ -1303,91 +1316,124 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
return "Ask anything, / for commands, @ for context..." return "Ask anything, / for commands, @ for context..."
} }
const modelControl = () => ( const modelControlState = createMemo<ComposerModelControlState>(() => ({
<Show when={!providersLoading()}> loading: providersLoading(),
<Show paid: providers.paid().length > 0,
when={providers.paid().length > 0} title: language.t("command.model.choose"),
fallback={ keybind: command.keybind("model.choose"),
<TooltipKeybind model: local.model,
placement="top" providerID: local.model.current()?.provider?.id,
gutter={4} modelName: local.model.current()?.name ?? language.t("dialog.model.select.title"),
title={language.t("command.model.choose")} style: control(),
keybind={command.keybind("model.choose")} onClose: restoreFocus,
> onUnpaidClick: () => {
<Button void import("@/components/dialog-select-model-unpaid").then((x) => {
data-action="prompt-model" dialog.show(() => <x.DialogSelectModelUnpaid model={local.model} />)
as="div" })
variant="ghost" },
size="normal" }))
class="min-w-0 max-w-[220px] justify-start text-[13px] font-[440] leading-4 text-v2-text-text-faint group"
style={control()}
onClick={() => {
void import("@/components/dialog-select-model-unpaid").then((x) => {
dialog.show(() => <x.DialogSelectModelUnpaid model={local.model} />)
})
}}
>
<Show when={local.model.current()?.provider?.id}>
<ProviderIcon
id={local.model.current()?.provider?.id ?? ""}
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
/>
</Show>
<span class="truncate">{local.model.current()?.name ?? language.t("dialog.model.select.title")}</span>
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
</Button>
</TooltipKeybind>
}
>
<TooltipKeybind
placement="top"
gutter={4}
title={language.t("command.model.choose")}
keybind={command.keybind("model.choose")}
>
<ModelSelectorPopover
model={local.model}
triggerAs={Button}
triggerProps={{
variant: "ghost",
size: "normal",
style: control(),
class:
"min-w-0 max-w-[220px] justify-start text-[13px] font-[440] leading-4 text-v2-text-text-faint group",
"data-action": "prompt-model",
}}
onClose={restoreFocus}
>
<Show when={local.model.current()?.provider?.id}>
<ProviderIcon
id={local.model.current()?.provider?.id ?? ""}
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
/>
</Show>
<span class="truncate">{local.model.current()?.name ?? language.t("dialog.model.select.title")}</span>
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
</ModelSelectorPopover>
</TooltipKeybind>
</Show>
</Show>
)
const newSession = () => props.variant === "new-session" const newSession = () => props.variant === "new-session"
const worktrees = createMemo(() => [MAIN_WORKTREE, ...(sync.project?.sandboxes ?? []), CREATE_WORKTREE]) const projects = createMemo(() => layout.projects.list())
const currentWorktree = createMemo(() => { const projectForDirectory = (directory: string | undefined) => {
if (worktrees().includes(props.newSessionWorktree ?? MAIN_WORKTREE)) if (!directory) return
return props.newSessionWorktree ?? MAIN_WORKTREE const key = pathKey(directory)
return MAIN_WORKTREE return projects().find(
(project) => pathKey(project.worktree) === key || project.sandboxes?.some((sandbox) => pathKey(sandbox) === key),
)
}
const selectedProject = createMemo(() => projectForDirectory(sdk.directory))
const projectResults = createMemo(() => {
const search = picker.projectSearch.trim().toLowerCase()
if (!search) return projects()
return projects().filter((project) => displayName(project).toLowerCase().includes(search))
}) })
const worktreeLabel = (value: string) => { const showAgentControl = createMemo(() => settings.general.showCustomAgents() && agentNames().length > 0)
if (value === MAIN_WORKTREE) return MAIN_WORKTREE const selectProject = (worktree: string) => {
if (value === CREATE_WORKTREE) return language.t("session.new.worktree.create") setPicker({
return getFilename(value) projectOpen: false,
projectSearch: "",
})
if (pathKey(worktree) === pathKey(selectedProject()?.worktree ?? "")) {
restoreFocus()
return
}
layout.projects.open(worktree)
server.projects.touch(worktree)
navigate(`/${base64Encode(worktree)}/session`)
}
const addProject = async () => {
const select = (result: string | string[] | null) => {
const directory = Array.isArray(result) ? result[0] : result
if (!directory) return
selectProject(directory)
}
if (platform.openDirectoryPickerDialog && server.isLocal()) {
select(await platform.openDirectoryPickerDialog({ title: language.t("command.project.open") }))
return
}
void import("@/components/dialog-select-directory").then((x) => {
dialog.show(
() => <x.DialogSelectDirectory onSelect={select} />,
() => select(null),
)
})
} }
const USE_V2_INPUT = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod" const projectPickerState = createMemo<ComposerPickerState>(() => ({
open: picker.projectOpen,
trigger: {
action: "prompt-project",
icon: "folder",
label: selectedProject() ? displayName(selectedProject()!) : language.t("session.new.project.new"),
class: "max-w-[203px]",
style: control(),
onPress: () => setPicker("projectOpen", true),
},
search: picker.projectSearch,
searchPlaceholder: language.t("session.new.project.search"),
clearLabel: language.t("common.clear"),
items: projectResults().map((project) => ({
icon: "folder",
label: displayName(project),
selected: selectedProject()?.worktree === project.worktree,
onSelect: () => selectProject(project.worktree),
})),
action: {
icon: "plus",
label: language.t("session.new.project.add"),
onSelect: () => {
setPicker("projectOpen", false)
void addProject()
},
},
onOpenChange: (open) => {
setPicker("projectOpen", open)
if (open) requestAnimationFrame(() => projectSearchRef?.focus())
},
onSearchInput: (value) => setPicker("projectSearch", value),
onSearchClear: () => setPicker("projectSearch", ""),
searchRef: (el) => (projectSearchRef = el),
}))
const agentControlState = createMemo<ComposerAgentControlState>(() => ({
title: language.t("command.agent.cycle"),
keybind: command.keybind("agent.cycle"),
options: agentNames(),
current: local.agent.current()?.name ?? "",
style: control(),
onSelect: (value) => {
local.agent.set(value)
restoreFocus()
},
}))
const newProjectTriggerState = createMemo<ComposerPickerTriggerState>(() => ({
action: "prompt-project",
icon: "folder-add-left",
label: language.t("session.new.project.new"),
class: "max-w-[160px]",
style: control(),
onPress: () => void addProject(),
}))
return ( return (
<div class="relative size-full flex flex-col gap-0"> <div class="relative size-full flex flex-col gap-0">
@@ -1408,155 +1454,146 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
t={(key) => language.t(key as Parameters<typeof language.t>[0])} t={(key) => language.t(key as Parameters<typeof language.t>[0])}
/> />
<Switch> <Switch>
<Match when={USE_V2_INPUT}> <Match when={settings.general.newLayoutDesigns()}>
<DockShellForm <div class="flex flex-col gap-3">
data-component={newSession() ? "session-new-composer" : "session-composer"} <DockShellForm
onSubmit={handleSubmit} data-component={newSession() ? "session-new-composer" : "session-composer"}
classList={{ onSubmit={handleSubmit}
"group/prompt-input min-h-[96px] w-full rounded-xl bg-v2-background-bg-base shadow-[var(--v2-elevation-raised)]": true, classList={{
"border-icon-info-active border-dashed": store.draggingType !== null, "group/prompt-input min-h-[96px] w-full rounded-xl bg-v2-background-bg-base shadow-[var(--v2-elevation-raised)]": true,
[props.class ?? ""]: !!props.class, "border-icon-info-active border-dashed": store.draggingType !== null,
}} [props.class ?? ""]: !!props.class,
>
<PromptDragOverlay
type={store.draggingType}
label={language.t(
store.draggingType === "@mention" ? "prompt.dropzone.file.label" : "prompt.dropzone.label",
)}
/>
<PromptContextItems
items={contextItems()}
active={(item) => {
const active = comments.active()
return !!item.commentID && item.commentID === active?.id && item.path === active?.file
}}
openComment={openComment}
remove={(item) => {
if (item.commentID) comments.remove(item.path, item.commentID)
prompt.context.remove(item.key)
}}
t={(key) => language.t(key as Parameters<typeof language.t>[0])}
/>
<PromptImageAttachments
attachments={imageAttachments()}
onOpen={(attachment) =>
dialog.show(() => <ImagePreview src={attachment.dataUrl} alt={attachment.filename} />)
}
onRemove={removeAttachment}
removeLabel={language.t("prompt.attachment.remove")}
/>
<div
class="relative min-h-[52px]"
onMouseDown={(e) => {
const target = e.target
if (!(target instanceof HTMLElement)) return
if (target.closest('[data-action="prompt-attach"], [data-action="prompt-submit"]')) return
editorRef?.focus()
}} }}
> >
<div class="relative max-h-[180px] overflow-y-auto no-scrollbar" ref={(el) => (scrollRef = el)}> <PromptDragOverlay
<div type={store.draggingType}
data-component="prompt-input" label={language.t(
ref={(el) => { store.draggingType === "@mention" ? "prompt.dropzone.file.label" : "prompt.dropzone.label",
editorRef = el )}
props.ref?.(el) />
}} <PromptContextItems
role="textbox" items={contextItems()}
aria-multiline="true" active={(item) => {
aria-label={designPlaceholder()} const active = comments.active()
contenteditable="true" return !!item.commentID && item.commentID === active?.id && item.path === active?.file
autocapitalize={store.mode === "normal" ? "sentences" : "off"} }}
autocorrect={store.mode === "normal" ? "on" : "off"} openComment={openComment}
spellcheck={store.mode === "normal"} remove={(item) => {
inputMode="text" if (item.commentID) comments.remove(item.path, item.commentID)
// @ts-expect-error prompt.context.remove(item.key)
autocomplete="off" }}
onInput={handleInput} t={(key) => language.t(key as Parameters<typeof language.t>[0])}
onPaste={handlePaste} />
onCompositionStart={handleCompositionStart} <PromptImageAttachments
onCompositionEnd={handleCompositionEnd} attachments={imageAttachments()}
onBlur={handleBlur} onOpen={(attachment) =>
onKeyDown={handleKeyDown} dialog.show(() => <ImagePreview src={attachment.dataUrl} alt={attachment.filename} />)
classList={{ }
"select-text": true, onRemove={removeAttachment}
"min-h-[52px] w-full px-4 pt-4 pb-2 focus:outline-none whitespace-pre-wrap leading-5 text-[13px] font-[440] text-v2-text-text-faint [font-family:Inter,var(--font-family-sans)]": true, removeLabel={language.t("prompt.attachment.remove")}
"[&_[data-type=file]]:text-syntax-property": true, />
"[&_[data-type=agent]]:text-syntax-type": true, <div
"font-mono!": store.mode === "shell", class="relative min-h-[52px]"
}} onMouseDown={(e) => {
/> const target = e.target
<div if (!(target instanceof HTMLElement)) return
data-component={newSession() ? "session-new-design-text" : "session-composer-text"} if (target.closest('[data-action^="prompt-"]')) return
class="absolute top-0 inset-x-0 px-4 pt-4 pointer-events-none whitespace-nowrap truncate leading-5 text-[13px] font-[440] text-v2-text-text-faint [font-family:Inter,var(--font-family-sans)]" editorRef?.focus()
classList={{ "font-mono!": store.mode === "shell", hidden: prompt.dirty() }} }}
> >
{designPlaceholder()} <div class="relative max-h-[180px] overflow-y-auto no-scrollbar" ref={(el) => (scrollRef = el)}>
<div
data-component="prompt-input"
ref={(el) => {
editorRef = el
props.ref?.(el)
}}
role="textbox"
aria-multiline="true"
aria-label={designPlaceholder()}
contenteditable="true"
autocapitalize={store.mode === "normal" ? "sentences" : "off"}
autocorrect={store.mode === "normal" ? "on" : "off"}
spellcheck={store.mode === "normal"}
inputMode="text"
// @ts-expect-error
autocomplete="off"
onInput={handleInput}
onPaste={handlePaste}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
classList={{
"select-text": true,
"min-h-[52px] w-full px-4 pt-4 pb-2 focus:outline-none whitespace-pre-wrap leading-5 text-[13px] font-[440] text-v2-text-text-base": true,
"[&_[data-type=file]]:text-syntax-property": true,
"[&_[data-type=agent]]:text-syntax-type": true,
"font-mono!": store.mode === "shell",
}}
/>
<div
data-component={newSession() ? "session-new-design-text" : "session-composer-text"}
class="absolute top-0 inset-x-0 px-4 pt-4 pointer-events-none whitespace-nowrap truncate leading-5 text-[13px] font-[440] text-v2-text-text-faint [font-family:Inter,var(--font-family-sans)]"
classList={{ "font-mono!": store.mode === "shell", hidden: prompt.dirty() }}
>
{designPlaceholder()}
</div>
</div> </div>
</div> </div>
</div> <div class="flex h-11 items-center px-2">
<div class="flex h-11 items-center px-2"> <div class="flex min-w-0 flex-1 items-center gap-0">
<div class="flex min-w-0 flex-1 items-center gap-0"> {fileAttachmentInput()}
{fileAttachmentInput()} <TooltipKeybind
<TooltipKeybind placement="top"
placement="top" title={language.t("prompt.action.attachFile")}
title={language.t("prompt.action.attachFile")} keybind={command.keybind("file.attach")}
keybind={command.keybind("file.attach")} >
> <IconButton
<IconButton data-action="prompt-attach"
data-action="prompt-attach" type="button"
type="button" icon="plus"
icon="plus"
variant="ghost"
class="size-7 rounded-md p-[6px] text-v2-icon-icon-muted"
style={buttons()}
onClick={pick}
disabled={store.mode !== "normal"}
tabIndex={store.mode === "normal" ? undefined : -1}
aria-label={language.t("prompt.action.attachFile")}
/>
</TooltipKeybind>
<Show when={newSession()}>
<div class="relative">
<div class="pointer-events-none absolute left-2 top-1/2 z-10 flex size-4 -translate-y-1/2 items-center justify-center">
<Icon name="sliders" size="small" />
</div>
<Select
size="normal"
options={worktrees()}
current={currentWorktree()}
label={worktreeLabel}
onSelect={(value) => {
if (value) props.onNewSessionWorktreeChange?.(value)
restoreFocus()
}}
class="max-w-[175px] justify-start text-text-base [&_[data-component=icon]]:text-v2-icon-icon-muted"
valueClass="truncate pl-5 text-[13px] font-[440] leading-4 text-v2-text-text-faint"
triggerStyle={control()}
triggerProps={{ "data-action": "prompt-workspace" }}
variant="ghost" variant="ghost"
class="size-7 rounded-md p-[6px] text-v2-icon-icon-muted"
style={buttons()}
onClick={pick}
disabled={store.mode !== "normal"}
tabIndex={store.mode === "normal" ? undefined : -1}
aria-label={language.t("prompt.action.attachFile")}
/> />
</div> </TooltipKeybind>
</Show> <Show when={showAgentControl()}>
{modelControl()} <ComposerAgentControl state={agentControlState()} />
</Show>
<Show when={newSession() && !selectedProject()}>
<ComposerPickerTrigger state={newProjectTriggerState()} />
</Show>
<ComposerModelControl state={modelControlState()} />
</div>
<Tooltip placement="top" inactive={!working() && blank()} value={tip()}>
<IconButton
data-action="prompt-submit"
type="submit"
disabled={!working() && blank()}
tabIndex={store.mode === "normal" ? undefined : -1}
icon={stopping() ? "stop" : store.mode === "shell" ? "arrow-undo-down" : "arrow-up"}
variant="primary"
class="size-7 rounded-md p-[6px] text-v2-icon-icon-muted shadow-[var(--v2-elevation-button-contrast)] disabled:opacity-50"
style={{
"background-image":
"linear-gradient(180deg,var(--v2-alpha-light-20) 0%,var(--v2-alpha-light-0) 100%),linear-gradient(90deg,var(--v2-background-bg-contrast) 0%,var(--v2-background-bg-contrast) 100%)",
}}
aria-label={stopping() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
/>
</Tooltip>
</div> </div>
<Tooltip placement="top" inactive={!working() && blank()} value={tip()}> </DockShellForm>
<IconButton <Show when={newSession() && selectedProject()}>
data-action="prompt-submit" <div class="flex h-7 min-w-0 items-center gap-0 px-2">
type="submit" <ComposerPicker state={projectPickerState()} />
disabled={!working() && blank()} </div>
tabIndex={store.mode === "normal" ? undefined : -1} </Show>
icon={stopping() ? "stop" : store.mode === "shell" ? "arrow-undo-down" : "arrow-up"} </div>
variant="primary"
class="size-7 rounded-md p-[6px] text-v2-icon-icon-muted shadow-[var(--v2-elevation-button-contrast)] disabled:opacity-50"
style={{
"background-image":
"linear-gradient(180deg,var(--v2-alpha-light-20) 0%,var(--v2-alpha-light-0) 100%),linear-gradient(90deg,var(--v2-background-bg-contrast) 0%,var(--v2-background-bg-contrast) 100%)",
}}
aria-label={stopping() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
/>
</Tooltip>
</div>
</DockShellForm>
</Match> </Match>
<Match when> <Match when>
<DockShellForm <DockShellForm
@@ -1892,3 +1929,226 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</div> </div>
) )
} }
type ComposerPickerItemState = {
icon: IconProps["name"]
label: string
selected?: boolean
onSelect: () => void
}
type ComposerPickerTriggerState = {
action: string
icon?: IconProps["name"]
label: string
class?: string
style: JSX.CSSProperties | undefined
onPress: () => void
}
type ComposerPickerState = {
open: boolean
trigger: ComposerPickerTriggerState
search: string
searchPlaceholder: string
clearLabel: string
items: ComposerPickerItemState[]
action: ComposerPickerItemState
listClass?: string
searchRef: (el: HTMLInputElement) => void
onOpenChange: (open: boolean) => void
onSearchInput: (value: string) => void
onSearchClear: () => void
}
type ComposerAgentControlState = {
title: string
keybind: string
options: string[]
current: string
style: JSX.CSSProperties | undefined
onSelect: (value: string | undefined) => void
}
type ComposerModelControlState = {
loading: boolean
paid: boolean
title: string
keybind: string
model: ReturnType<typeof useLocal>["model"]
providerID?: string
modelName: string
style: JSX.CSSProperties | undefined
onClose: () => void
onUnpaidClick: () => void
}
function ComposerPickerTrigger(props: ComponentProps<"button"> & { state: ComposerPickerTriggerState }) {
const [local, rest] = splitProps(props, ["state", "class", "style", "onClick"])
return (
<button
{...rest}
data-action={local.state.action}
type="button"
class={`flex h-7 min-w-0 items-center gap-1.5 rounded px-2 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-faint transition-colors hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none ${local.state.class ?? ""}`}
style={local.state.style}
onClick={() => local.state.onPress()}
>
<Show when={local.state.icon}>
{(icon) => <Icon name={icon()} size="small" class="shrink-0 text-v2-icon-icon-muted" />}
</Show>
<span class="min-w-0 truncate leading-5">{local.state.label}</span>
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
</button>
)
}
function ComposerPickerMenuItem(props: { state: ComposerPickerItemState }) {
return (
<button
type="button"
class="flex h-7 w-full items-center gap-2 rounded px-3 text-left text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
onClick={props.state.onSelect}
>
<Icon name={props.state.icon} size="small" class="shrink-0 text-v2-icon-icon-base" />
<span class="min-w-0 flex-1 truncate leading-5">{props.state.label}</span>
<Show when={props.state.selected}>
<Icon name="check-small" size="small" class="shrink-0 text-v2-icon-icon-base" />
</Show>
</button>
)
}
function ComposerPicker(props: { state: ComposerPickerState }) {
return (
<KobaltePopover
open={props.state.open}
placement="bottom-start"
gutter={4}
modal={false}
onOpenChange={props.state.onOpenChange}
>
<KobaltePopover.Trigger as={ComposerPickerTrigger} state={props.state.trigger} />
<KobaltePopover.Portal>
<KobaltePopover.Content
class="w-[243px] overflow-hidden rounded-md bg-v2-background-bg-layer-01 shadow-[var(--v2-elevation-floating)] focus:outline-none"
onOpenAutoFocus={(event) => event.preventDefault()}
>
<div class={`flex flex-col p-0.5 ${props.state.listClass ?? ""}`}>
<div class="flex h-7 items-center gap-2 rounded px-3 text-v2-icon-icon-muted">
<Icon name="magnifying-glass" size="small" class="shrink-0" />
<input
ref={props.state.searchRef}
value={props.state.search}
placeholder={props.state.searchPlaceholder}
class="h-7 min-w-0 flex-1 border-0 bg-transparent text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base outline-none placeholder:text-v2-text-text-faint"
onInput={(event) => props.state.onSearchInput(event.currentTarget.value)}
/>
<Show when={props.state.search.trim()}>
<button
type="button"
class="flex size-5 items-center justify-center rounded text-v2-icon-icon-muted hover:bg-v2-overlay-simple-overlay-hover"
onClick={props.state.onSearchClear}
aria-label={props.state.clearLabel}
>
<Icon name="close-small" size="small" />
</button>
</Show>
</div>
<For each={props.state.items}>{(item) => <ComposerPickerMenuItem state={item} />}</For>
</div>
<div class="h-px bg-v2-border-border-muted" />
<div class="flex flex-col p-0.5">
<ComposerPickerMenuItem state={props.state.action} />
</div>
</KobaltePopover.Content>
</KobaltePopover.Portal>
</KobaltePopover>
)
}
function ComposerAgentControl(props: { state: ComposerAgentControlState }) {
return (
<div class="relative">
<div class="pointer-events-none absolute left-2 top-1/2 z-10 flex size-4 -translate-y-1/2 items-center justify-center text-v2-icon-icon-muted">
<Icon name="sliders" size="small" />
</div>
<TooltipKeybind placement="top" gutter={4} title={props.state.title} keybind={props.state.keybind}>
<Select
size="normal"
options={props.state.options}
current={props.state.current}
onSelect={props.state.onSelect}
class="max-w-[175px] justify-start text-v2-text-text-faint [&_[data-component=icon]]:text-v2-icon-icon-muted"
valueClass="truncate pl-5 text-[13px] font-[440] leading-5 text-v2-text-text-faint"
triggerStyle={props.state.style}
triggerProps={{ "data-action": "prompt-agent" }}
variant="ghost"
/>
</TooltipKeybind>
</div>
)
}
function ComposerModelControl(props: { state: ComposerModelControlState }) {
return (
<Show when={!props.state.loading}>
<Show
when={props.state.paid}
fallback={
<TooltipKeybind placement="top" gutter={4} title={props.state.title} keybind={props.state.keybind}>
<Button
data-action="prompt-model"
as="div"
variant="ghost"
size="normal"
class="min-w-0 max-w-[220px] justify-start text-[13px] font-[440] leading-5 text-v2-text-text-faint group"
style={props.state.style}
onClick={props.state.onUnpaidClick}
>
<Show when={props.state.providerID}>
{(providerID) => (
<ProviderIcon
id={providerID()}
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
/>
)}
</Show>
<span class="truncate">{props.state.modelName}</span>
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
</Button>
</TooltipKeybind>
}
>
<TooltipKeybind placement="top" gutter={4} title={props.state.title} keybind={props.state.keybind}>
<ModelSelectorPopover
model={props.state.model}
triggerAs={Button}
triggerProps={{
variant: "ghost",
size: "normal",
style: props.state.style,
class:
"min-w-0 max-w-[220px] justify-start text-[13px] font-[440] leading-5 text-v2-text-text-faint group",
"data-action": "prompt-model",
}}
onClose={props.state.onClose}
>
<Show when={props.state.providerID}>
{(providerID) => (
<ProviderIcon
id={providerID()}
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
/>
)}
</Show>
<span class="truncate">{props.state.modelName}</span>
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
</ModelSelectorPopover>
</TooltipKeybind>
</Show>
</Show>
)
}
@@ -162,8 +162,8 @@ beforeAll(async () => {
}), }),
})) }))
mock.module("@/context/global-sync", () => ({ mock.module("@/context/server-sync", () => ({
useGlobalSync: () => ({ useServerSync: () => ({
child: (directory: string) => { child: (directory: string) => {
syncedDirectories.push(directory) syncedDirectories.push(directory)
storedSessions[directory] ??= [] storedSessions[directory] ??= []
@@ -5,7 +5,7 @@ import { Binary } from "@opencode-ai/core/util/binary"
import { useNavigate, useParams } from "@solidjs/router" import { useNavigate, useParams } from "@solidjs/router"
import { batch, type Accessor } from "solid-js" import { batch, type Accessor } from "solid-js"
import type { FileSelection } from "@/context/file" import type { FileSelection } from "@/context/file"
import { useGlobalSync } from "@/context/global-sync" import { useServerSync } from "@/context/server-sync"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout" import { useLayout } from "@/context/layout"
import { useLocal } from "@/context/local" import { useLocal } from "@/context/local"
@@ -38,7 +38,7 @@ export type FollowupDraft = {
type FollowupSendInput = { type FollowupSendInput = {
client: ReturnType<typeof useSDK>["client"] client: ReturnType<typeof useSDK>["client"]
globalSync: ReturnType<typeof useGlobalSync> serverSync: ReturnType<typeof useServerSync>
sync: ReturnType<typeof useSync> sync: ReturnType<typeof useSync>
draft: FollowupDraft draft: FollowupDraft
messageID?: string messageID?: string
@@ -53,7 +53,7 @@ const draftImages = (prompt: Prompt) => prompt.filter((part): part is ImageAttac
export async function sendFollowupDraft(input: FollowupSendInput) { export async function sendFollowupDraft(input: FollowupSendInput) {
const text = draftText(input.draft.prompt) const text = draftText(input.draft.prompt)
const images = draftImages(input.draft.prompt) const images = draftImages(input.draft.prompt)
const [, setStore] = input.globalSync.child(input.draft.sessionDirectory) const [, setStore] = input.serverSync.child(input.draft.sessionDirectory)
const setBusy = () => { const setBusy = () => {
if (!input.optimisticBusy) return if (!input.optimisticBusy) return
@@ -205,7 +205,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
const navigate = useNavigate() const navigate = useNavigate()
const sdk = useSDK() const sdk = useSDK()
const sync = useSync() const sync = useSync()
const globalSync = useGlobalSync() const serverSync = useServerSync()
const local = useLocal() const local = useLocal()
const permission = usePermission() const permission = usePermission()
const prompt = usePrompt() const prompt = usePrompt()
@@ -226,8 +226,8 @@ export function createPromptSubmit(input: PromptSubmitInput) {
const sessionID = params.id const sessionID = params.id
if (!sessionID) return Promise.resolve() if (!sessionID) return Promise.resolve()
globalSync.todo.set(sessionID, []) serverSync.todo.set(sessionID, [])
const [, setStore] = globalSync.child(sdk.directory) const [, setStore] = serverSync.child(sdk.directory)
setStore("todo", sessionID, []) setStore("todo", sessionID, [])
input.onAbort?.() input.onAbort?.()
@@ -273,7 +273,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
} }
const seed = (dir: string, info: Session) => { const seed = (dir: string, info: Session) => {
const [, setStore] = globalSync.child(dir) const [, setStore] = serverSync.child(dir)
setStore("session", (list: Session[]) => { setStore("session", (list: Session[]) => {
const result = Binary.search(list, info.id, (item) => item.id) const result = Binary.search(list, info.id, (item) => item.id)
const next = [...list] const next = [...list]
@@ -354,7 +354,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
directory: sessionDirectory, directory: sessionDirectory,
throwOnError: true, throwOnError: true,
}) })
globalSync.child(sessionDirectory) serverSync.child(sessionDirectory)
} }
input.onNewSessionWorktreeReset?.() input.onNewSessionWorktreeReset?.()
@@ -557,7 +557,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
void sendFollowupDraft({ void sendFollowupDraft({
client, client,
sync, sync,
globalSync, serverSync,
draft, draft,
messageID, messageID,
optimisticBusy: sessionDirectory === projectDirectory, optimisticBusy: sessionDirectory === projectDirectory,
@@ -52,7 +52,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
}), }),
) )
const metrics = createMemo(() => getSessionContextMetrics(messages(), providers.all())) const metrics = createMemo(() => getSessionContextMetrics(messages(), [...providers.all().values()]))
const context = createMemo(() => metrics().context) const context = createMemo(() => metrics().context)
const cost = createMemo(() => { const cost = createMemo(() => {
return usd().format(metrics().totalCost) return usd().format(metrics().totalCost)
@@ -132,7 +132,7 @@ export function SessionContextTab() {
}), }),
) )
const metrics = createMemo(() => getSessionContextMetrics(messages(), providers.all())) const metrics = createMemo(() => getSessionContextMetrics(messages(), [...providers.all().values()]))
const ctx = createMemo(() => metrics().context) const ctx = createMemo(() => metrics().context)
const formatter = createMemo(() => createSessionContextFormatter(language.intl())) const formatter = createMemo(() => createSessionContextFormatter(language.intl()))
@@ -24,7 +24,9 @@ import { useSessionLayout } from "@/pages/session/session-layout"
import { messageAgentColor } from "@/utils/agent" import { messageAgentColor } from "@/utils/agent"
import { decode64 } from "@/utils/base64" import { decode64 } from "@/utils/base64"
import { Persist, persisted } from "@/utils/persist" import { Persist, persisted } from "@/utils/persist"
import { StatusPopover } from "../status-popover" import { StatusPopover, StatusPopoverV2 } from "../status-popover"
import { IconButtonV2 } from "@opencode-ai/ui/v2/components/icon-button-v2.jsx"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/components/icon.jsx"
const OPEN_APPS = [ const OPEN_APPS = [
"vscode", "vscode",
@@ -153,11 +155,11 @@ export function SessionHeader() {
}) })
const hotkey = createMemo(() => command.keybind("file.open")) const hotkey = createMemo(() => command.keybind("file.open"))
const os = createMemo(() => detectOS(platform)) const os = createMemo(() => detectOS(platform))
const isDesktopBeta = platform.platform === "desktop" && import.meta.env.VITE_OPENCODE_CHANNEL === "beta" const isDesktopV2 = createMemo(() => platform.platform === "desktop" && settings.general.newLayoutDesigns())
const search = createMemo(() => !isDesktopBeta || settings.general.showSearch()) const search = createMemo(() => (isDesktopV2() ? settings.general.showSearch() : true))
const tree = createMemo(() => !isDesktopBeta || settings.general.showFileTree()) const tree = createMemo(() => (isDesktopV2() ? settings.general.showFileTree() : true))
const term = createMemo(() => !isDesktopBeta || settings.general.showTerminal()) const term = createMemo(() => (isDesktopV2() ? settings.general.showTerminal() : true))
const status = createMemo(() => !isDesktopBeta || settings.general.showStatus()) const status = createMemo(() => (isDesktopV2() ? settings.general.showStatus() : true))
const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({ const [exists, setExists] = createStore<Partial<Record<OpenApp, boolean>>>({
finder: true, finder: true,
@@ -231,6 +233,14 @@ export function SessionHeader() {
const tint = createMemo(() => const tint = createMemo(() =>
messageAgentColor(params.id ? sync.data.message[params.id] : undefined, sync.data.agent), messageAgentColor(params.id ? sync.data.message[params.id] : undefined, sync.data.agent),
) )
const v2ActionsState = createMemo<SessionHeaderV2ActionsState>(() => ({
statusVisible: status(),
statusLabel: language.t("status.popover.trigger"),
reviewLabel: language.t("command.review.toggle"),
reviewKeybind: command.keybind("review.toggle"),
reviewOpened: view().reviewPanel.opened(),
onReviewToggle: () => view().reviewPanel.toggle(),
}))
const selectApp = (app: OpenApp) => { const selectApp = (app: OpenApp) => {
if (!options().some((item) => item.id === app)) return if (!options().some((item) => item.id === app)) return
@@ -311,193 +321,235 @@ export function SessionHeader() {
<Show when={rightMount()}> <Show when={rightMount()}>
{(mount) => ( {(mount) => (
<Portal mount={mount()}> <Portal mount={mount()}>
<div class="flex items-center gap-2"> <Show
<Show when={projectDirectory()}> when={isDesktopV2}
<div class="hidden xl:flex items-center"> fallback={
<Show <div class="flex items-center gap-2">
when={canOpen()} <Show when={projectDirectory()}>
fallback={ <div class="hidden xl:flex items-center">
<div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden"> <Show
<Button when={canOpen()}
variant="ghost" fallback={
class="rounded-none h-full py-0 pr-3 pl-0.5 gap-1.5 border-none shadow-none" <div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden">
onClick={copyPath} <Button
aria-label={language.t("session.header.open.copyPath")} variant="ghost"
> class="rounded-none h-full py-0 pr-3 pl-0.5 gap-1.5 border-none shadow-none"
<Icon name="copy" size="small" class="text-icon-base" /> onClick={copyPath}
<span class="text-12-regular text-text-strong"> aria-label={language.t("session.header.open.copyPath")}
{language.t("session.header.open.copyPath")} >
</span> <Icon name="copy" size="small" class="text-icon-base" />
</Button> <span class="text-12-regular text-text-strong">
</div> {language.t("session.header.open.copyPath")}
} </span>
> </Button>
<div class="flex items-center">
<div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden">
<Button
variant="ghost"
class="rounded-none h-full px-0.5 border-none shadow-none disabled:!cursor-default"
classList={{
"bg-surface-raised-base-active": opening(),
}}
onClick={() => openDir(current().id)}
disabled={opening()}
aria-label={language.t("session.header.open.ariaLabel", { app: current().label })}
>
<div class="flex size-5 shrink-0 items-center justify-center [&_[data-component=app-icon]]:size-5">
<Show when={opening()} fallback={<AppIcon id={current().icon} />}>
<Spinner class="size-3.5" style={{ color: tint() ?? "var(--icon-base)" }} />
</Show>
</div> </div>
</Button> }
<DropdownMenu >
gutter={4} <div class="flex items-center">
placement="bottom-end" <div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden">
open={menu.open} <Button
onOpenChange={(open) => setMenu("open", open)} variant="ghost"
> class="rounded-none h-full px-0.5 border-none shadow-none disabled:!cursor-default"
<DropdownMenu.Trigger classList={{
as={IconButton} "bg-surface-raised-base-active": opening(),
icon="chevron-down" }}
variant="ghost" onClick={() => openDir(current().id)}
disabled={opening()} disabled={opening()}
class="rounded-none h-full w-[20px] p-0 border-none shadow-none data-[expanded]:bg-surface-raised-base-active disabled:!cursor-default" aria-label={language.t("session.header.open.ariaLabel", { app: current().label })}
classList={{ >
"bg-surface-raised-base-active": opening(), <div class="flex size-5 shrink-0 items-center justify-center [&_[data-component=app-icon]]:size-5">
}} <Show when={opening()} fallback={<AppIcon id={current().icon} />}>
aria-label={language.t("session.header.open.menu")} <Spinner class="size-3.5" style={{ color: tint() ?? "var(--icon-base)" }} />
/> </Show>
<DropdownMenu.Portal> </div>
<DropdownMenu.Content class="[&_[data-slot=dropdown-menu-item]]:pl-1 [&_[data-slot=dropdown-menu-radio-item]]:pl-1 [&_[data-slot=dropdown-menu-radio-item]+[data-slot=dropdown-menu-radio-item]]:mt-1"> </Button>
<DropdownMenu.Group> <DropdownMenu
<DropdownMenu.GroupLabel class="!px-1 !py-1"> gutter={4}
{language.t("session.header.openIn")} placement="bottom-end"
</DropdownMenu.GroupLabel> open={menu.open}
<DropdownMenu.RadioGroup onOpenChange={(open) => setMenu("open", open)}
class="mt-1" >
value={current().id} <DropdownMenu.Trigger
onChange={(value) => { as={IconButton}
if (!OPEN_APPS.includes(value as OpenApp)) return icon="chevron-down"
selectApp(value as OpenApp) variant="ghost"
}} disabled={opening()}
> class="rounded-none h-full w-[20px] p-0 border-none shadow-none data-[expanded]:bg-surface-raised-base-active disabled:!cursor-default"
<For each={options()}> classList={{
{(o) => ( "bg-surface-raised-base-active": opening(),
<DropdownMenu.RadioItem
value={o.id}
disabled={opening()}
onSelect={() => {
setMenu("open", false)
openDir(o.id)
}}
>
<div class="flex size-5 shrink-0 items-center justify-center [&_[data-component=app-icon]]:size-5">
<AppIcon id={o.icon} />
</div>
<DropdownMenu.ItemLabel>{o.label}</DropdownMenu.ItemLabel>
<DropdownMenu.ItemIndicator>
<Icon name="check-small" size="small" class="text-icon-weak" />
</DropdownMenu.ItemIndicator>
</DropdownMenu.RadioItem>
)}
</For>
</DropdownMenu.RadioGroup>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item
onSelect={() => {
setMenu("open", false)
copyPath()
}} }}
> aria-label={language.t("session.header.open.menu")}
<div class="flex size-5 shrink-0 items-center justify-center"> />
<Icon name="copy" size="small" class="text-icon-weak" /> <DropdownMenu.Portal>
</div> <DropdownMenu.Content class="[&_[data-slot=dropdown-menu-item]]:pl-1 [&_[data-slot=dropdown-menu-radio-item]]:pl-1 [&_[data-slot=dropdown-menu-radio-item]+[data-slot=dropdown-menu-radio-item]]:mt-1">
<DropdownMenu.ItemLabel> <DropdownMenu.Group>
{language.t("session.header.open.copyPath")} <DropdownMenu.GroupLabel class="!px-1 !py-1">
</DropdownMenu.ItemLabel> {language.t("session.header.openIn")}
</DropdownMenu.Item> </DropdownMenu.GroupLabel>
</DropdownMenu.Content> <DropdownMenu.RadioGroup
</DropdownMenu.Portal> class="mt-1"
</DropdownMenu> value={current().id}
</div> onChange={(value) => {
if (!OPEN_APPS.includes(value as OpenApp)) return
selectApp(value as OpenApp)
}}
>
<For each={options()}>
{(o) => (
<DropdownMenu.RadioItem
value={o.id}
disabled={opening()}
onSelect={() => {
setMenu("open", false)
openDir(o.id)
}}
>
<div class="flex size-5 shrink-0 items-center justify-center [&_[data-component=app-icon]]:size-5">
<AppIcon id={o.icon} />
</div>
<DropdownMenu.ItemLabel>{o.label}</DropdownMenu.ItemLabel>
<DropdownMenu.ItemIndicator>
<Icon name="check-small" size="small" class="text-icon-weak" />
</DropdownMenu.ItemIndicator>
</DropdownMenu.RadioItem>
)}
</For>
</DropdownMenu.RadioGroup>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item
onSelect={() => {
setMenu("open", false)
copyPath()
}}
>
<div class="flex size-5 shrink-0 items-center justify-center">
<Icon name="copy" size="small" class="text-icon-weak" />
</div>
<DropdownMenu.ItemLabel>
{language.t("session.header.open.copyPath")}
</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
</div>
</div>
</Show>
</div> </div>
</Show> </Show>
</div> <div class="flex items-center gap-1">
</Show> <Show when={status()}>
<div class="flex items-center gap-1"> <Tooltip placement="bottom" value={language.t("status.popover.trigger")}>
<Show when={status()}> <StatusPopover />
<Tooltip placement="bottom" value={language.t("status.popover.trigger")}> </Tooltip>
<StatusPopover /> </Show>
</Tooltip> <Show when={term()}>
</Show> <TooltipKeybind
<Show when={term()}> title={language.t("command.terminal.toggle")}
<TooltipKeybind keybind={command.keybind("terminal.toggle")}
title={language.t("command.terminal.toggle")}
keybind={command.keybind("terminal.toggle")}
>
<Button
variant="ghost"
class="group/terminal-toggle titlebar-icon w-8 h-6 p-0 box-border shrink-0"
onClick={toggleTerminal}
aria-label={language.t("command.terminal.toggle")}
aria-expanded={view().terminal.opened()}
aria-controls="terminal-panel"
>
<Icon size="small" name={view().terminal.opened() ? "terminal-active" : "terminal"} />
</Button>
</TooltipKeybind>
</Show>
<div class="hidden md:flex items-center gap-1 shrink-0">
<TooltipKeybind
title={language.t("command.review.toggle")}
keybind={command.keybind("review.toggle")}
>
<Button
variant="ghost"
class="group/review-toggle titlebar-icon w-8 h-6 p-0 box-border"
onClick={() => view().reviewPanel.toggle()}
aria-label={language.t("command.review.toggle")}
aria-expanded={view().reviewPanel.opened()}
aria-controls="review-panel"
>
<Icon size="small" name={view().reviewPanel.opened() ? "review-active" : "review"} />
</Button>
</TooltipKeybind>
<Show when={tree()}>
<TooltipKeybind
title={language.t("command.fileTree.toggle")}
keybind={command.keybind("fileTree.toggle")}
>
<Button
variant="ghost"
class="titlebar-icon w-8 h-6 p-0 box-border"
onClick={() => layout.fileTree.toggle()}
aria-label={language.t("command.fileTree.toggle")}
aria-expanded={layout.fileTree.opened()}
aria-controls="file-tree-panel"
> >
<div class="relative flex items-center justify-center size-4"> <Button
<Icon variant="ghost"
size="small" class="group/terminal-toggle titlebar-icon w-8 h-6 p-0 box-border shrink-0"
name={layout.fileTree.opened() ? "file-tree-active" : "file-tree"} onClick={toggleTerminal}
classList={{ aria-label={language.t("command.terminal.toggle")}
"text-icon-strong": layout.fileTree.opened(), aria-expanded={view().terminal.opened()}
"text-icon-weak": !layout.fileTree.opened(), aria-controls="terminal-panel"
}} >
/> <Icon size="small" name={view().terminal.opened() ? "terminal-active" : "terminal"} />
</div> </Button>
</Button> </TooltipKeybind>
</TooltipKeybind> </Show>
</Show>
<div class="hidden md:flex items-center gap-1 shrink-0">
<TooltipKeybind
title={language.t("command.review.toggle")}
keybind={command.keybind("review.toggle")}
>
<Button
variant="ghost"
class="group/review-toggle titlebar-icon w-8 h-6 p-0 box-border"
onClick={() => view().reviewPanel.toggle()}
aria-label={language.t("command.review.toggle")}
aria-expanded={view().reviewPanel.opened()}
aria-controls="review-panel"
>
<Icon size="small" name={view().reviewPanel.opened() ? "review-active" : "review"} />
</Button>
</TooltipKeybind>
<Show when={tree()}>
<TooltipKeybind
title={language.t("command.fileTree.toggle")}
keybind={command.keybind("fileTree.toggle")}
>
<Button
variant="ghost"
class="titlebar-icon w-8 h-6 p-0 box-border"
onClick={() => layout.fileTree.toggle()}
aria-label={language.t("command.fileTree.toggle")}
aria-expanded={layout.fileTree.opened()}
aria-controls="file-tree-panel"
>
<div class="relative flex items-center justify-center size-4">
<Icon
size="small"
name={layout.fileTree.opened() ? "file-tree-active" : "file-tree"}
classList={{
"text-icon-strong": layout.fileTree.opened(),
"text-icon-weak": !layout.fileTree.opened(),
}}
/>
</div>
</Button>
</TooltipKeybind>
</Show>
</div>
</div>
</div> </div>
</div> }
</div> >
<SessionHeaderV2Actions state={v2ActionsState()} />
</Show>
</Portal> </Portal>
)} )}
</Show> </Show>
</> </>
) )
} }
type SessionHeaderV2ActionsState = {
statusVisible: boolean
statusLabel: string
reviewLabel: string
reviewKeybind: string
reviewOpened: boolean
onReviewToggle: () => void
}
function SessionHeaderV2Actions(props: { state: SessionHeaderV2ActionsState }) {
return (
<div class="flex items-center gap-0">
<Show when={props.state.statusVisible}>
<Tooltip placement="bottom" value={props.state.statusLabel}>
<StatusPopoverV2 />
</Tooltip>
</Show>
<TooltipKeybind title={props.state.reviewLabel} keybind={props.state.reviewKeybind}>
<IconButtonV2
type="button"
variant="ghost-muted"
size="large"
class="!w-9 shrink-0"
state={props.state.reviewOpened ? "pressed" : undefined}
onClick={props.state.onReviewToggle}
aria-label={props.state.reviewLabel}
aria-expanded={props.state.reviewOpened}
aria-controls="review-panel"
icon={<IconV2 name="sidebar-right" />}
/>
</TooltipKeybind>
</div>
)
}
@@ -1,76 +1,13 @@
import type { JSX } from "solid-js" import type { JSX } from "solid-js"
import { createMemo } from "solid-js"
import { useNavigate } from "@solidjs/router"
import { useGlobalSync } from "@/context/global-sync"
import { useLayout } from "@/context/layout"
import { useSDK } from "@/context/sdk"
import { useServer } from "@/context/server"
import { useSync } from "@/context/sync"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { getFilename } from "@opencode-ai/core/util/path"
import { Icon } from "@opencode-ai/ui/icon"
import { Select } from "@opencode-ai/ui/select"
import { WordmarkV2 } from "@opencode-ai/ui/v2/components/wordmark-v2.jsx" import { WordmarkV2 } from "@opencode-ai/ui/v2/components/wordmark-v2.jsx"
const MAIN_WORKTREE = "main" export function NewSessionDesignView(props: { children: JSX.Element }) {
export function NewSessionDesignView(props: { worktree: string; children: JSX.Element }) {
const globalSync = useGlobalSync()
const layout = useLayout()
const navigate = useNavigate()
const sdk = useSDK()
const server = useServer()
const sync = useSync()
const projectRoot = createMemo(() => sync.project?.worktree ?? sdk.directory)
const projects = createMemo(() => {
const roots = globalSync.data.project.map((project) => project.worktree)
if (roots.includes(projectRoot())) return roots
return [projectRoot(), ...roots]
})
const branch = createMemo(() => sync.data.vcs?.branch ?? MAIN_WORKTREE)
const openProject = (directory: string | undefined) => {
if (!directory) return
if (directory === projectRoot()) return
layout.projects.open(directory)
server.projects.touch(directory)
navigate(`/${base64Encode(directory)}/session`)
}
return ( return (
<div data-component="session-new-design" class="relative size-full overflow-hidden bg-v2-background-bg-deep"> <div data-component="session-new-design" class="relative size-full overflow-hidden bg-v2-background-bg-deep">
<div class="absolute inset-x-0 top-[25.375%] flex justify-center px-6"> <div class="absolute inset-x-0 top-[25.375%] flex justify-center px-6">
<div class="w-full max-w-[720px]"> <div class="w-full max-w-[720px]">
<WordmarkV2 class="h-auto w-full text-v2-icon-icon-base" /> <WordmarkV2 class="h-auto w-full text-v2-icon-icon-base" />
<div class="mt-8"> <div class="mt-8">{props.children}</div>
{props.children}
<div class="mt-3 flex h-7 items-center gap-0 pl-2">
<Select
size="normal"
variant="ghost"
options={projects()}
current={projectRoot()}
label={getFilename}
onSelect={openProject}
class="max-w-[203px] justify-start text-text-base [&_[data-component=icon]]:text-v2-icon-icon-muted"
valueClass="truncate text-[length:13px] font-[440] text-v2-text-text-faint"
/>
<div class="relative">
<div class="pointer-events-none absolute left-2 top-1/2 z-10 flex size-4 -translate-y-1/2 items-center justify-center">
<Icon name="branch" size="small" />
</div>
<Select
size="normal"
variant="ghost"
options={[branch()]}
current={branch()}
class="max-w-[240px] justify-start text-text-base [&_[data-component=icon]]:text-v2-icon-icon-muted"
valueClass="truncate pl-5 font-[440] text-v2-text-text-faint"
/>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -12,8 +12,8 @@ import { useParams } from "@solidjs/router"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { usePermission } from "@/context/permission" import { usePermission } from "@/context/permission"
import { usePlatform, type DisplayBackend } from "@/context/platform" import { usePlatform, type DisplayBackend } from "@/context/platform"
import { useGlobalSync } from "@/context/global-sync" import { useServerSync } from "@/context/server-sync"
import { useGlobalSDK } from "@/context/global-sdk" import { useServerSDK } from "@/context/server-sdk"
import { import {
monoDefault, monoDefault,
monoFontFamily, monoFontFamily,
@@ -175,8 +175,8 @@ export const SettingsGeneral: Component = () => {
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) }))) const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
const globalSync = useGlobalSync() const serverSync = useServerSync()
const globalSdk = useGlobalSDK() const globalSdk = useServerSDK()
const [shells] = createResource( const [shells] = createResource(
() => () =>
@@ -204,11 +204,11 @@ export const SettingsGeneral: Component = () => {
}) })
const autoOption = { id: "auto", value: "", label: language.t("settings.general.row.shell.autoDefault") } const autoOption = { id: "auto", value: "", label: language.t("settings.general.row.shell.autoDefault") }
const currentShell = createMemo(() => globalSync.data.config.shell ?? "") const currentShell = createMemo(() => serverSync.data.config.shell ?? "")
const shellOptions = createMemo<ShellSelectOption[]>(() => { const shellOptions = createMemo<ShellSelectOption[]>(() => {
const list = shells.latest const list = shells.latest
const current = globalSync.data.config.shell const current = serverSync.data.config.shell
const nameCounts = new Map<string, number>() const nameCounts = new Map<string, number>()
for (const s of list) { for (const s of list) {
@@ -343,7 +343,7 @@ export const SettingsGeneral: Component = () => {
onSelect={(option) => { onSelect={(option) => {
if (!option) return if (!option) return
if (option.value === currentShell()) return if (option.value === currentShell()) return
globalSync.updateConfig({ shell: option.value }) serverSync.updateConfig({ shell: option.value })
}} }}
variant="secondary" variant="secondary"
size="small" size="small"
@@ -399,6 +399,18 @@ export const SettingsGeneral: Component = () => {
/> />
</div> </div>
</SettingsRow> </SettingsRow>
<SettingsRow
title={language.t("settings.general.row.newLayoutDesigns.title")}
description={language.t("settings.general.row.newLayoutDesigns.description")}
>
<div data-action="settings-new-layout-designs">
<Switch
checked={settings.general.newLayoutDesigns()}
onChange={(checked) => settings.general.setNewLayoutDesigns(checked)}
/>
</div>
</SettingsRow>
</SettingsList> </SettingsList>
</div> </div>
) )
@@ -444,18 +456,6 @@ export const SettingsGeneral: Component = () => {
</div> </div>
</SettingsRow> </SettingsRow>
<SettingsRow
title={language.t("settings.general.row.showTerminal.title")}
description={language.t("settings.general.row.showTerminal.description")}
>
<div data-action="settings-show-terminal">
<Switch
checked={settings.general.showTerminal()}
onChange={(checked) => settings.general.setShowTerminal(checked)}
/>
</div>
</SettingsRow>
<SettingsRow <SettingsRow
title={language.t("settings.general.row.showStatus.title")} title={language.t("settings.general.row.showStatus.title")}
description={language.t("settings.general.row.showStatus.description")} description={language.t("settings.general.row.showStatus.description")}
@@ -467,6 +467,18 @@ export const SettingsGeneral: Component = () => {
/> />
</div> </div>
</SettingsRow> </SettingsRow>
<SettingsRow
title={language.t("settings.general.row.showCustomAgents.title")}
description={language.t("settings.general.row.showCustomAgents.description")}
>
<div data-action="settings-show-custom-agents">
<Switch
checked={settings.general.showCustomAgents()}
onChange={(checked) => settings.general.setShowCustomAgents(checked)}
/>
</div>
</SettingsRow>
</SettingsList> </SettingsList>
</div> </div>
) )
@@ -781,7 +793,6 @@ export const SettingsGeneral: Component = () => {
</Show> </Show>
) )
console.log(import.meta.env)
return ( return (
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10"> <div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]"> <div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
@@ -803,7 +814,7 @@ export const SettingsGeneral: Component = () => {
<DisplaySection /> <DisplaySection />
<Show when={desktop() && import.meta.env.VITE_OPENCODE_CHANNEL === "beta"}> <Show when={desktop()}>
<AdvancedSection /> <AdvancedSection />
</Show> </Show>
</div> </div>
@@ -6,8 +6,8 @@ import { showToast } from "@opencode-ai/ui/toast"
import { popularProviders, useProviders } from "@/hooks/use-providers" import { popularProviders, useProviders } from "@/hooks/use-providers"
import { createMemo, type Component, For, Show } from "solid-js" import { createMemo, type Component, For, Show } from "solid-js"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useGlobalSDK } from "@/context/global-sdk" import { useServerSDK } from "@/context/server-sdk"
import { useGlobalSync } from "@/context/global-sync" import { useServerSync } from "@/context/server-sync"
import { DialogConnectProvider } from "./dialog-connect-provider" import { DialogConnectProvider } from "./dialog-connect-provider"
import { DialogSelectProvider } from "./dialog-select-provider" import { DialogSelectProvider } from "./dialog-select-provider"
import { DialogCustomProvider } from "./dialog-custom-provider" import { DialogCustomProvider } from "./dialog-custom-provider"
@@ -30,8 +30,8 @@ const PROVIDER_NOTES = [
export const SettingsProviders: Component = () => { export const SettingsProviders: Component = () => {
const dialog = useDialog() const dialog = useDialog()
const language = useLanguage() const language = useLanguage()
const globalSDK = useGlobalSDK() const serverSDK = useServerSDK()
const globalSync = useGlobalSync() const serverSync = useServerSync()
const providers = useProviders() const providers = useProviders()
const connected = createMemo(() => { const connected = createMemo(() => {
@@ -74,7 +74,7 @@ export const SettingsProviders: Component = () => {
const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key
const isConfigCustom = (providerID: string) => { const isConfigCustom = (providerID: string) => {
const provider = globalSync.data.config.provider?.[providerID] const provider = serverSync.data.config.provider?.[providerID]
if (!provider) return false if (!provider) return false
if (provider.npm !== "@ai-sdk/openai-compatible") return false if (provider.npm !== "@ai-sdk/openai-compatible") return false
if (!provider.models || Object.keys(provider.models).length === 0) return false if (!provider.models || Object.keys(provider.models).length === 0) return false
@@ -82,11 +82,11 @@ export const SettingsProviders: Component = () => {
} }
const disableProvider = async (providerID: string, name: string) => { const disableProvider = async (providerID: string, name: string) => {
const before = globalSync.data.config.disabled_providers ?? [] const before = serverSync.data.config.disabled_providers ?? []
const next = before.includes(providerID) ? before : [...before, providerID] const next = before.includes(providerID) ? before : [...before, providerID]
globalSync.set("config", "disabled_providers", next) serverSync.set("config", "disabled_providers", next)
await globalSync await serverSync
.updateConfig({ disabled_providers: next }) .updateConfig({ disabled_providers: next })
.then(() => { .then(() => {
showToast({ showToast({
@@ -97,7 +97,7 @@ export const SettingsProviders: Component = () => {
}) })
}) })
.catch((err: unknown) => { .catch((err: unknown) => {
globalSync.set("config", "disabled_providers", before) serverSync.set("config", "disabled_providers", before)
const message = err instanceof Error ? err.message : String(err) const message = err instanceof Error ? err.message : String(err)
showToast({ title: language.t("common.requestFailed"), description: message }) showToast({ title: language.t("common.requestFailed"), description: message })
}) })
@@ -105,14 +105,14 @@ export const SettingsProviders: Component = () => {
const disconnect = async (providerID: string, name: string) => { const disconnect = async (providerID: string, name: string) => {
if (isConfigCustom(providerID)) { if (isConfigCustom(providerID)) {
await globalSDK.client.auth.remove({ providerID }).catch(() => undefined) await serverSDK.client.auth.remove({ providerID }).catch(() => undefined)
await disableProvider(providerID, name) await disableProvider(providerID, name)
return return
} }
await globalSDK.client.auth await serverSDK.client.auth
.remove({ providerID }) .remove({ providerID })
.then(async () => { .then(async () => {
await globalSDK.client.global.dispose() await serverSDK.client.global.dispose()
showToast({ showToast({
variant: "success", variant: "success",
icon: "circle-check", icon: "circle-check",
@@ -7,16 +7,17 @@ import { useMutation, useQueryClient } from "@tanstack/solid-query"
import { showToast } from "@opencode-ai/ui/toast" import { showToast } from "@opencode-ai/ui/toast"
import { useNavigate } from "@solidjs/router" import { useNavigate } from "@solidjs/router"
import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js" import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js"
import { createStore, reconcile } from "solid-js/store" import { createStore } from "solid-js/store"
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row" import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform" import { usePlatform } from "@/context/platform"
import { useSDK } from "@/context/sdk" import { useSDK } from "@/context/sdk"
import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server" import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health" import { type ServerHealth } from "@/utils/server-health"
import { useQueryOptions } from "@/context/global-sync" import { useQueryOptions } from "@/context/server-sync"
import { pathKey } from "@/utils/path-key" import { pathKey } from "@/utils/path-key"
import { useServers } from "@/context/servers"
const pollMs = 10_000 const pollMs = 10_000
@@ -54,40 +55,6 @@ const listServersByHealth = (
}) })
} }
const useServerHealth = (servers: Accessor<ServerConnection.Any[]>, enabled: Accessor<boolean>) => {
const checkServerHealth = useCheckServerHealth()
const [status, setStatus] = createStore({} as Record<ServerConnection.Key, ServerHealth | undefined>)
createEffect(() => {
if (!enabled()) {
setStatus(reconcile({}))
return
}
const list = servers()
let dead = false
const refresh = async () => {
const results: Record<string, ServerHealth> = {}
await Promise.all(
list.map(async (conn) => {
results[ServerConnection.key(conn)] = await checkServerHealth(conn.http)
}),
)
if (dead) return
setStatus(reconcile(results))
}
void refresh()
const id = setInterval(() => void refresh(), pollMs)
onCleanup(() => {
dead = true
clearInterval(id)
})
})
return status
}
const useDefaultServerKey = ( const useDefaultServerKey = (
get: (() => string | Promise<string | null | undefined> | null | undefined) | undefined, get: (() => string | Promise<string | null | undefined> | null | undefined) | undefined,
) => { ) => {
@@ -166,8 +133,162 @@ const useMcpToggleMutation = () => {
})) }))
} }
type ServerStatusState = {
servers: () => ServerStatusItem[]
defaultKey: () => ServerConnection.Key | undefined
ariaLabel: string
serversLabel: string
defaultLabel: string
manageLabel: string
onManage: () => void
}
type ServerStatusItem = {
key: ServerConnection.Key
conn: ServerConnection.Any
health?: ServerHealth
blocked: boolean
active: boolean
onSelect: () => void
}
export function StatusPopoverServerBody() {
const servers = useServers()
const server = useServer()
const platform = usePlatform()
const dialog = useDialog()
const language = useLanguage()
const navigate = useNavigate()
let dialogRun = 0
let dialogDead = false
onCleanup(() => {
dialogDead = true
dialogRun += 1
})
const sortedServers = createMemo(() => listServersByHealth(servers.list(), server.key, servers.health))
const defaultServer = useDefaultServerKey(platform.getDefaultServer)
const serverItems = createMemo(() =>
sortedServers().map((conn) => {
const key = ServerConnection.key(conn)
return {
key,
conn,
health: servers.health[key],
blocked: servers.health[key]?.healthy === false,
active: !!server.current && key === ServerConnection.key(server.current),
onSelect: () => {
navigate("/")
queueMicrotask(() => server.setActive(key))
},
}
}),
)
return (
<ServerStatusPopoverView
state={{
servers: serverItems,
defaultKey: defaultServer.key,
ariaLabel: language.t("status.popover.ariaLabel"),
serversLabel: language.t("status.popover.tab.servers"),
defaultLabel: language.t("common.default"),
manageLabel: language.t("status.popover.action.manageServers"),
onManage: () => {
const run = ++dialogRun
void import("./dialog-select-server").then((x) => {
if (dialogDead || dialogRun !== run) return
dialog.show(() => <x.DialogSelectServer />, defaultServer.refresh)
})
},
}}
/>
)
}
function ServerStatusPopoverView(props: { state: ServerStatusState }) {
return (
<div class="flex items-center gap-1 w-[360px] rounded-xl shadow-[var(--shadow-lg-border-base)]">
<Tabs
aria-label={props.state.ariaLabel}
class="tabs bg-background-strong rounded-xl overflow-hidden"
data-component="tabs"
data-active="servers"
defaultValue="servers"
variant="alt"
>
<Tabs.List data-slot="tablist" class="bg-transparent border-b-0 px-4 pt-2 pb-0 gap-4 h-10">
<Tabs.Trigger value="servers" data-slot="tab" class="text-12-regular">
{props.state.servers().length > 0 ? `${props.state.servers().length} ` : ""}
{props.state.serversLabel}
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="servers">
<ServerStatusList state={props.state} />
</Tabs.Content>
</Tabs>
</div>
)
}
function ServerStatusList(props: { state: ServerStatusState }) {
return (
<div class="flex flex-col px-2 pb-2">
<div class="flex flex-col p-3 bg-background-base rounded-sm min-h-14">
<For each={props.state.servers()}>
{(item) => {
return (
<button
type="button"
class="flex items-center gap-2 w-full h-8 pl-3 pr-1.5 py-1.5 rounded-md transition-colors text-left"
classList={{
"hover:bg-surface-raised-base-hover": !item.blocked,
"cursor-not-allowed": item.blocked,
}}
aria-disabled={item.blocked}
onClick={() => {
if (item.blocked) return
item.onSelect()
}}
>
<ServerHealthIndicator health={item.health} />
<ServerRow
conn={item.conn}
dimmed={item.blocked}
status={item.health}
class="flex items-center gap-2 w-full min-w-0"
nameClass="text-14-regular text-text-base truncate"
versionClass="text-12-regular text-text-weak truncate"
badge={
<Show when={item.key === props.state.defaultKey()}>
<span class="text-11-regular text-text-base bg-surface-base px-1.5 py-0.5 rounded-md">
{props.state.defaultLabel}
</span>
</Show>
}
>
<div class="flex-1" />
<Show when={item.active}>
<Icon name="check" size="small" class="text-icon-weak shrink-0" />
</Show>
</ServerRow>
</button>
)
}}
</For>
<Button variant="secondary" class="mt-3 self-start h-8 px-3 py-1.5" onClick={props.state.onManage}>
{props.state.manageLabel}
</Button>
</div>
</div>
)
}
export function StatusPopoverBody(props: { shown: Accessor<boolean> }) { export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
const sync = useSync() const sync = useSync()
const servers = useServers()
const server = useServer() const server = useServer()
const platform = usePlatform() const platform = usePlatform()
const dialog = useDialog() const dialog = useDialog()
@@ -192,15 +313,7 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
dialogDead = true dialogDead = true
dialogRun += 1 dialogRun += 1
}) })
const servers = createMemo(() => { const sortedServers = createMemo(() => listServersByHealth(servers.list(), server.key, servers.health))
const current = server.current
const list = server.list
if (!current) return list
if (list.every((item) => ServerConnection.key(item) !== ServerConnection.key(current))) return [current, ...list]
return [current, ...list.filter((item) => ServerConnection.key(item) !== ServerConnection.key(current))]
})
const health = useServerHealth(servers, props.shown)
const sortedServers = createMemo(() => listServersByHealth(servers(), server.key, health))
const toggleMcp = useMcpToggleMutation() const toggleMcp = useMcpToggleMutation()
const defaultServer = useDefaultServerKey(platform.getDefaultServer) const defaultServer = useDefaultServerKey(platform.getDefaultServer)
const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b))) const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b)))
@@ -226,7 +339,7 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
> >
<Tabs.List data-slot="tablist" class="bg-transparent border-b-0 px-4 pt-2 pb-0 gap-4 h-10"> <Tabs.List data-slot="tablist" class="bg-transparent border-b-0 px-4 pt-2 pb-0 gap-4 h-10">
<Tabs.Trigger value="servers" data-slot="tab" class="text-12-regular"> <Tabs.Trigger value="servers" data-slot="tab" class="text-12-regular">
{sortedServers().length > 0 ? `${sortedServers().length} ` : ""} {servers.list().length > 0 ? `${servers.list().length} ` : ""}
{language.t("status.popover.tab.servers")} {language.t("status.popover.tab.servers")}
</Tabs.Trigger> </Tabs.Trigger>
<Tabs.Trigger value="mcp" data-slot="tab" class="text-12-regular"> <Tabs.Trigger value="mcp" data-slot="tab" class="text-12-regular">
@@ -249,7 +362,7 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
<For each={sortedServers()}> <For each={sortedServers()}>
{(s) => { {(s) => {
const key = ServerConnection.key(s) const key = ServerConnection.key(s)
const blocked = () => health[key]?.healthy === false const blocked = () => servers.health[key]?.healthy === false
return ( return (
<button <button
type="button" type="button"
@@ -265,11 +378,11 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
queueMicrotask(() => server.setActive(key)) queueMicrotask(() => server.setActive(key))
}} }}
> >
<ServerHealthIndicator health={health[key]} /> <ServerHealthIndicator health={servers.health[key]} />
<ServerRow <ServerRow
conn={s} conn={s}
dimmed={blocked()} dimmed={blocked()}
status={health[key]} status={servers.health[key]}
class="flex items-center gap-2 w-full min-w-0" class="flex items-center gap-2 w-full min-w-0"
nameClass="text-14-regular text-text-base truncate" nameClass="text-14-regular text-text-base truncate"
versionClass="text-12-regular text-text-weak truncate" versionClass="text-12-regular text-text-weak truncate"
+144 -7
View File
@@ -1,19 +1,24 @@
import { Button } from "@opencode-ai/ui/button" import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon" import { Icon } from "@opencode-ai/ui/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/components/icon-button-v2.jsx"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/components/icon.jsx"
import { Popover } from "@opencode-ai/ui/popover" import { Popover } from "@opencode-ai/ui/popover"
import { Suspense, createMemo, createSignal, lazy, Show } from "solid-js" import { Suspense, createMemo, createSignal, lazy, Show, type JSX } from "solid-js"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useServer } from "@/context/server" import { useServer } from "@/context/server"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
import { useServers } from "@/context/servers"
const Body = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverBody }))) const Body = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverBody })))
const ServerBody = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverServerBody })))
export function StatusPopover() { export function StatusPopover() {
const language = useLanguage() const language = useLanguage()
const server = useServer() const server = useServer()
const servers = useServers()
const sync = useSync() const sync = useSync()
const [shown, setShown] = createSignal(false) const [shown, setShown] = createSignal(false)
const ready = createMemo(() => server.healthy() === false || sync.data.mcp_ready) const ready = createMemo(() => servers.health[server.key]?.healthy === false || sync.data.mcp_ready)
const mcpIssue = createMemo(() => { const mcpIssue = createMemo(() => {
const mcp = Object.values(sync.data.mcp ?? {}) const mcp = Object.values(sync.data.mcp ?? {})
const failed = mcp.some((item) => item.status === "failed" || item.status === "needs_client_registration") const failed = mcp.some((item) => item.status === "failed" || item.status === "needs_client_registration")
@@ -21,7 +26,8 @@ export function StatusPopover() {
if (failed) return "critical" as const if (failed) return "critical" as const
if (warn) return "warning" as const if (warn) return "warning" as const
}) })
const healthy = createMemo(() => server.healthy() === true && !mcpIssue()) const serverHealthy = () => servers.health[server.key]?.healthy === true
const healthy = createMemo(() => servers.health[server.key]?.healthy === true && !mcpIssue())
return ( return (
<Popover <Popover
@@ -43,10 +49,9 @@ export function StatusPopover() {
classList={{ classList={{
"absolute -top-px -right-px size-1.5 rounded-full": true, "absolute -top-px -right-px size-1.5 rounded-full": true,
"bg-icon-success-base": ready() && healthy(), "bg-icon-success-base": ready() && healthy(),
"bg-icon-warning-base": ready() && server.healthy() === true && mcpIssue() === "warning", "bg-icon-warning-base": ready() && serverHealthy() && mcpIssue() === "warning",
"bg-icon-critical-base": "bg-icon-critical-base": serverHealthy() || (ready() && serverHealthy() && mcpIssue() === "critical"),
server.healthy() === false || (ready() && server.healthy() === true && mcpIssue() === "critical"), "bg-border-weak-base": serverHealthy() || !ready(),
"bg-border-weak-base": server.healthy() === undefined || !ready(),
}} }}
/> />
</div> </div>
@@ -68,3 +73,135 @@ export function StatusPopover() {
</Popover> </Popover>
) )
} }
export function StatusPopoverV2(props: { scope?: "server" }) {
if (props.scope === "server") return <ServerStatusPopover />
return <DirectoryStatusPopover />
}
function DirectoryStatusPopover() {
const language = useLanguage()
const server = useServer()
const servers = useServers()
const sync = useSync()
const [shown, setShown] = createSignal(false)
const serverHealth = () => servers.health[server.key]?.healthy
const ready = createMemo(() => serverHealth() === false || sync.data.mcp_ready)
const mcpIssue = createMemo(() => {
const mcp = Object.values(sync.data.mcp ?? {})
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(() => serverHealth() === true && !mcpIssue())
const state = createMemo<StatusPopoverState>(() => ({
shown: shown(),
ready: ready(),
healthy: healthy(),
serverHealth: serverHealth(),
issue: mcpIssue(),
label: language.t("status.popover.trigger"),
onOpenChange: setShown,
body: () => (
<StatusPopoverBody shown={shown()}>
<Body shown={shown} />
</StatusPopoverBody>
),
}))
return <StatusPopoverView state={state()} />
}
function ServerStatusPopover() {
const language = useLanguage()
const server = useServer()
const servers = useServers()
const [shown, setShown] = createSignal(false)
const serverHealth = () => servers.health[server.key]?.healthy
const state = createMemo<StatusPopoverState>(() => ({
shown: shown(),
ready: serverHealth() !== undefined,
healthy: serverHealth() === true,
serverHealth: serverHealth(),
label: language.t("status.popover.trigger"),
onOpenChange: setShown,
body: () => (
<StatusPopoverBody shown={shown()}>
<ServerBody />
</StatusPopoverBody>
),
}))
return <StatusPopoverView state={state()} />
}
type StatusPopoverState = {
shown: boolean
ready: boolean
healthy: boolean
serverHealth: boolean | undefined
issue?: "critical" | "warning"
label: string
onOpenChange: (value: boolean) => void
body: () => JSX.Element
}
function StatusPopoverBody(props: { shown: boolean; children: JSX.Element }) {
return (
<Show when={props.shown}>
<Suspense
fallback={<div class="w-[360px] h-14 rounded-xl bg-background-strong shadow-[var(--shadow-lg-border-base)]" />}
>
{props.children}
</Suspense>
</Show>
)
}
function StatusPopoverView(props: { state: StatusPopoverState }) {
const statusDotClass = () => ({
"absolute rounded-full": true,
"bg-icon-success-base": props.state.ready && props.state.healthy,
"bg-icon-warning-base": props.state.ready && props.state.serverHealth === true && props.state.issue === "warning",
"bg-icon-critical-base":
props.state.serverHealth === false ||
(props.state.ready && props.state.serverHealth === true && props.state.issue === "critical"),
"bg-border-weak-base": props.state.serverHealth === undefined || !props.state.ready,
})
const popoverProps = {
class:
"[&_[data-slot=popover-body]]:p-0 w-[360px] max-w-[calc(100vw-40px)] bg-transparent border-0 shadow-none rounded-xl",
gutter: 4,
placement: "bottom-end" as const,
shift: -168,
}
return (
<Popover
open={props.state.shown}
onOpenChange={props.state.onOpenChange}
triggerAs={IconButtonV2}
triggerProps={{
variant: "ghost-muted",
size: "large",
class: "!w-9 shrink-0",
state: props.state.shown ? "pressed" : undefined,
"aria-label": props.state.label,
}}
trigger={
<div class="relative size-4">
<IconV2 name={props.state.shown ? "status-active" : "status"} />
<div
classList={statusDotClass()}
class="-top-1 -right-1 size-2 border border-[var(--v2-background-bg-deep)]"
/>
</div>
}
{...popoverProps}
>
{props.state.body()}
</Popover>
)
}
@@ -0,0 +1,28 @@
import { describe, expect, test } from "bun:test"
import { readSessionTabsRemovedDetail, SESSION_TABS_REMOVED_EVENT } from "./titlebar-session-events"
describe("titlebar session events", () => {
test("reads valid removed session tab details", () => {
expect(
readSessionTabsRemovedDetail(
new CustomEvent(SESSION_TABS_REMOVED_EVENT, {
detail: { directory: "/tmp/project", sessionIDs: ["ses_1", "ses_2", 1] },
}),
),
).toEqual({
directory: "/tmp/project",
sessionIDs: ["ses_1", "ses_2"],
})
})
test("ignores invalid removed session tab details", () => {
expect(readSessionTabsRemovedDetail(new Event(SESSION_TABS_REMOVED_EVENT))).toBeUndefined()
expect(
readSessionTabsRemovedDetail(
new CustomEvent(SESSION_TABS_REMOVED_EVENT, {
detail: { directory: "/tmp/project", sessionIDs: [] },
}),
),
).toBeUndefined()
})
})
@@ -0,0 +1,29 @@
export const SESSION_TABS_REMOVED_EVENT = "opencode:session-tabs-removed"
export type SessionTabsRemovedDetail = {
directory: string
sessionIDs: string[]
}
export function notifySessionTabsRemoved(input: SessionTabsRemovedDetail) {
window.dispatchEvent(new CustomEvent(SESSION_TABS_REMOVED_EVENT, { detail: input }))
}
export function readSessionTabsRemovedDetail(event: Event): SessionTabsRemovedDetail | undefined {
if (!(event instanceof CustomEvent)) return undefined
const detail: unknown = event.detail
if (!detail || typeof detail !== "object") return undefined
if (!("directory" in detail)) return undefined
if (!("sessionIDs" in detail)) return undefined
if (typeof detail.directory !== "string") return undefined
if (!Array.isArray(detail.sessionIDs)) return undefined
const sessionIDs = detail.sessionIDs.filter((id): id is string => typeof id === "string")
if (sessionIDs.length === 0) return undefined
return {
directory: detail.directory,
sessionIDs,
}
}
+189 -54
View File
@@ -16,13 +16,19 @@ import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings" import { useSettings } from "@/context/settings"
import { WindowsAppMenu } from "./windows-app-menu" import { WindowsAppMenu } from "./windows-app-menu"
import { applyPath, backPath, forwardPath } from "./titlebar-history" import { applyPath, backPath, forwardPath } from "./titlebar-history"
import { useGlobalSync } from "@/context/global-sync" import { useServerSync } from "@/context/server-sync"
import { decodeDirectory } from "@/pages/directory-layout" import { decodeDirectory } from "@/pages/directory-layout"
import { iife } from "@opencode-ai/core/util/iife" import { iife } from "@opencode-ai/core/util/iife"
import { base64Encode } from "@opencode-ai/core/util/encode" import { base64Encode } from "@opencode-ai/core/util/encode"
import { Avatar as AvatarV2 } from "@opencode-ai/ui/v2/components/avatar-v2.jsx" import { Avatar as AvatarV2 } from "@opencode-ai/ui/v2/components/avatar-v2.jsx"
import { displayName, getProjectAvatarSource, projectForSession } from "@/pages/layout/helpers" import { displayName, getProjectAvatarSource, projectForSession } from "@/pages/layout/helpers"
import { makeEventListener } from "@solid-primitives/event-listener" import { makeEventListener } from "@solid-primitives/event-listener"
import { StatusPopoverV2 } from "@/components/status-popover"
import {
readSessionTabsRemovedDetail,
SESSION_TABS_REMOVED_EVENT,
type SessionTabsRemovedDetail,
} from "@/components/titlebar-session-events"
type TauriDesktopWindow = { type TauriDesktopWindow = {
startDragging?: () => Promise<void> startDragging?: () => Promise<void>
@@ -49,7 +55,6 @@ const legacyTitlebarHeight = 40
const v2TitlebarHeight = 44 const v2TitlebarHeight = 44
const minTitlebarZoom = 0.25 const minTitlebarZoom = 0.25
const windowsControlsBaseWidth = 138 // 3 native Windows caption buttons at 46px each. const windowsControlsBaseWidth = 138 // 3 native Windows caption buttons at 46px each.
const USE_V2_TITLEBAR = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
const makeSessionHref = (b64Dir: string, sessionId: string) => `/${b64Dir}/session/${sessionId}` const makeSessionHref = (b64Dir: string, sessionId: string) => `/${b64Dir}/session/${sessionId}`
@@ -69,6 +74,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const params = useParams() const params = useParams()
const useV2Titlebar = createMemo(() => settings.general.newLayoutDesigns())
const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos") const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows") const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
@@ -79,7 +85,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
const titlebarZoom = () => (windows() ? Math.max(zoom(), minTitlebarZoom) : zoom()) const titlebarZoom = () => (windows() ? Math.max(zoom(), minTitlebarZoom) : zoom())
const counterZoom = () => (windows() && titlebarZoom() < 1 ? 1 / titlebarZoom() : 1) const counterZoom = () => (windows() && titlebarZoom() < 1 ? 1 / titlebarZoom() : 1)
const minHeight = () => { const minHeight = () => {
const height = USE_V2_TITLEBAR ? v2TitlebarHeight : legacyTitlebarHeight const height = useV2Titlebar() ? v2TitlebarHeight : legacyTitlebarHeight
if (mac()) return `${height / zoom()}px` if (mac()) return `${height / zoom()}px`
if (windows()) return `${height / Math.min(titlebarZoom(), 1)}px` if (windows()) return `${height / Math.min(titlebarZoom(), 1)}px`
return undefined return undefined
@@ -113,7 +119,23 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
const canBack = createMemo(() => history.index > 0) const canBack = createMemo(() => history.index > 0)
const canForward = createMemo(() => history.index < history.stack.length - 1) const canForward = createMemo(() => history.index < history.stack.length - 1)
const hasProjects = createMemo(() => layout.projects.list().length > 0) const hasProjects = createMemo(() => layout.projects.list().length > 0)
const nav = createMemo(() => import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" || settings.general.showNavigation()) const nav = createMemo(() => (useV2Titlebar() ? settings.general.showNavigation() : true))
const updateState = createMemo<TitlebarUpdatePillState>(() => {
const version = props.update?.version()
return {
visible: version !== undefined,
installing: props.update?.installing() ?? false,
label: "Update",
ariaLabel: language.t("toast.update.action.installRestart"),
title: version ? `Update ${version}` : undefined,
onInstall: () => props.update?.install(),
}
})
const v2RightState = createMemo<TitlebarV2RightState>(() => ({
update: updateState(),
statusVisible: !params.dir && settings.general.showStatus(),
statusLabel: language.t("status.popover.trigger"),
}))
const back = () => { const back = () => {
const next = backPath(history) const next = backPath(history)
@@ -200,8 +222,8 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
<header <header
classList={{ classList={{
"shrink-0 relative overflow-hidden flex flex-row": true, "shrink-0 relative overflow-hidden flex flex-row": true,
"h-11 bg-v2-background-bg-deep": USE_V2_TITLEBAR, "h-11 bg-v2-background-bg-deep": useV2Titlebar(),
"h-10 bg-background-base": !USE_V2_TITLEBAR, "h-10 bg-background-base": !useV2Titlebar(),
}} }}
style={{ style={{
"min-height": minHeight(), "min-height": minHeight(),
@@ -217,25 +239,19 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
onDblClick={maximize} onDblClick={maximize}
> >
<Switch> <Switch>
<Match when={USE_V2_TITLEBAR}> <Match when={useV2Titlebar()}>
{(_) => { {(_) => {
const globalSync = useGlobalSync() const serverSync = useServerSync()
const navigate = useNavigate() const navigate = useNavigate()
const homeMatch = useMatch(() => "/") const homeMatch = useMatch(() => "/")
const openNewSession = () => { const newSessionHref = () => {
if (params.dir) { if (params.dir) return `/${params.dir}/session`
navigate(`/${params.dir}/session`)
return
}
const project = layout.projects.list()[0] const project = layout.projects.list()[0]
if (!project) { if (!project) return "/"
navigate("/")
return
}
navigate(`/${base64Encode(project.worktree)}/session`) return `/${base64Encode(project.worktree)}/session`
} }
type Tab = { dir: string; sessionId: string; href: string } type Tab = { dir: string; sessionId: string; href: string }
@@ -265,7 +281,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
) )
}, },
removeTab: (href: string) => { removeTab: (href: string) => {
startTransition(() => { void startTransition(() => {
setStore( setStore(
produce((tabs) => { produce((tabs) => {
const index = tabs.findIndex((t) => t.href === href) const index = tabs.findIndex((t) => t.href === href)
@@ -278,11 +294,45 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
) )
}) })
}, },
removeSessions: (input: SessionTabsRemovedDetail) => {
void startTransition(() => {
setStore(
produce((tabs) => {
const sessionIDs = new Set(input.sessionIDs)
const currentHref = params.dir && params.id ? makeSessionHref(params.dir, params.id) : undefined
const currentIndex = currentHref ? tabs.findIndex((tab) => tab.href === currentHref) : -1
const removedCurrent =
currentIndex !== -1 &&
tabs[currentIndex]?.dir === input.directory &&
sessionIDs.has(tabs[currentIndex]?.sessionId ?? "")
for (let i = tabs.length - 1; i >= 0; i--) {
const tab = tabs[i]
if (!tab) continue
if (tab.dir !== input.directory) continue
if (!sessionIDs.has(tab.sessionId)) continue
tabs.splice(i, 1)
}
if (!removedCurrent) return
const nextTab = tabs[currentIndex] ?? tabs[tabs.length - 1]
if (nextTab) navigate(nextTab.href)
else navigate("/")
}),
)
})
},
} }
return [store, actions] return [store, actions]
}) })
makeEventListener(window, SESSION_TABS_REMOVED_EVENT, (event) => {
const detail = readSessionTabsRemovedDetail(event)
if (!detail) return
tabsStoreActions.removeSessions(detail)
})
createEffect(() => { createEffect(() => {
const params = useParams() const params = useParams()
if (!(params.dir && params.id)) return if (!(params.dir && params.id)) return
@@ -302,14 +352,21 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
const currentSessionTab = () => { const currentSessionTab = () => {
if (!params.dir || !params.id) return if (!params.dir || !params.id) return
const href = makeSessionHref(params.dir, params.id) const href = makeSessionHref(params.dir, params.id)
if (!tabsStore.some((tab) => tab.href === href)) return return tabsStore.find((tab) => tab.href === href)
return href
} }
const closeCurrentSessionTab = () => { const closeCurrentSessionTab = () => {
const href = currentSessionTab() const tab = currentSessionTab()
if (!href) return false if (!tab) return false
tabsStoreActions.removeTab(href) tabsStoreActions.removeTab(tab.href)
return true
}
const closeNewSessionTab = () => {
if (!(params.dir && !params.id)) return false
const last = tabsStore[tabsStore.length - 1]
if (last) navigate(last.href)
else navigate("/")
return true return true
} }
@@ -319,7 +376,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
(event) => { (event) => {
if (!event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return if (!event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return
if (event.key.toLowerCase() !== "w") return if (event.key.toLowerCase() !== "w") return
if (!closeCurrentSessionTab()) return if (!(closeCurrentSessionTab() || closeNewSessionTab())) return
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
@@ -327,11 +384,68 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
{ capture: true }, { capture: true },
) )
command.register(() => {
const commands = [
{
id: `tab.prev`,
category: "tab",
title: "",
keybind: `mod+option+ArrowLeft`,
hidden: true,
onSelect: () => {
let index = tabsStore.findIndex((tab) => tab.href === currentSessionTab()?.href)
if (index === -1) return
index -= 1
if (index === -1) index = tabsStore.length - 1
const next = tabsStore[index]
if (next) navigate(next.href)
},
},
{
id: `tab.next`,
category: "tab",
title: "",
keybind: `mod+option+ArrowRight`,
hidden: true,
onSelect: () => {
let index = tabsStore.findIndex((tab) => tab.href === currentSessionTab()?.href)
if (index === -1) return
index += 1
if (index === tabsStore.length) index = 0
const next = tabsStore[index]
if (next) navigate(next.href)
},
},
...Array.from({ length: 9 }, (_, i) => {
const index = i
const number = index + 1
return {
id: `tab.${number}`,
category: "tab",
title: "",
keybind: `mod+${number}`,
disabled: layout.projects.list().length <= index,
hidden: true,
onSelect: () => {
const tab = tabsStore[index]
if (tab) navigate(tab.href)
},
}
}),
]
return commands
})
const tabsEnriched = iife(() => { const tabsEnriched = iife(() => {
const base = mapArray( const base = mapArray(
() => tabsStore, () => tabsStore,
(tab) => { (tab) => {
const sync = globalSync.createDirSyncContext(tab.dir) const sync = serverSync.createDirSyncContext(tab.dir)
const session = sync.session.get(tab.sessionId) const session = sync.session.get(tab.sessionId)
return session ? { ...tab, info: session } : null return session ? { ...tab, info: session } : null
}, },
@@ -376,7 +490,6 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
project={projectForSession(tab.info, projects(), projectByID())} project={projectForSession(tab.info, projects(), projectByID())}
directory={tab.dir} directory={tab.dir}
onClose={() => tabsStoreActions.removeTab(tab.href)} onClose={() => tabsStoreActions.removeTab(tab.href)}
hideClose={tabsEnriched().length < 2}
/> />
</> </>
)} )}
@@ -391,7 +504,8 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
size="large" size="large"
class="shrink-0" class="shrink-0"
icon={<IconV2 name="plus" />} icon={<IconV2 name="plus" />}
onClick={openNewSession} as="a"
href={newSessionHref()}
aria-label={language.t("command.session.new")} aria-label={language.t("command.session.new")}
/> />
} }
@@ -404,7 +518,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
</Show> </Show>
<div class="min-w-0 flex-1" /> <div class="min-w-0 flex-1" />
</div> </div>
<TitlebarUpdatePill update={props.update} /> <TitlebarV2Right state={v2RightState()} />
<Show when={windows() && !electronWindows()}> <Show when={windows() && !electronWindows()}>
<div data-tauri-decorum-tb class="flex flex-row" /> <div data-tauri-decorum-tb class="flex flex-row" />
</Show> </Show>
@@ -571,28 +685,50 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
) )
} }
function TitlebarUpdatePill(props: { update?: TitlebarUpdate }) { type TitlebarUpdatePillState = {
const language = useLanguage() visible: boolean
const version = () => props.update?.version() installing: boolean
label: string
ariaLabel: string
title?: string
onInstall: () => void
}
type TitlebarV2RightState = {
update: TitlebarUpdatePillState
statusVisible: boolean
statusLabel: string
}
function TitlebarV2Right(props: { state: TitlebarV2RightState }) {
return ( return (
<Show when={version() !== undefined}> <div class="flex shrink-0 items-center justify-end gap-0">
<button <TitlebarUpdatePill state={props.state.update} />
type="button" <Show when={props.state.statusVisible}>
class="h-5 shrink-0 rounded-[27px] bg-[var(--v2-background-bg-accent)] px-2.5 text-[11px] font-[530] leading-[1.1] tracking-[-0.04px] text-[var(--v2-text-text-contrast)] disabled:opacity-60" <Tooltip placement="bottom" value={props.state.statusLabel}>
onClick={() => props.update?.install()} <StatusPopoverV2 scope="server" />
disabled={props.update?.installing()} </Tooltip>
aria-label={language.t("toast.update.action.installRestart")} </Show>
title={version() ? `Update ${version()}` : undefined} <div id="opencode-titlebar-right" class="flex shrink-0 items-center justify-end gap-0" />
> </div>
Update
</button>
</Show>
) )
} }
function DesktopTitlebarIconButton(props: Parameters<typeof IconButtonV2>[0]) { function TitlebarUpdatePill(props: { state: TitlebarUpdatePillState }) {
return return (
<Show when={props.state.visible}>
<button
type="button"
class="h-5 shrink-0 rounded-[27px] bg-[var(--v2-background-bg-layer-03)] px-2.5 text-[11px] font-[530] leading-4 tracking-[0.05px] text-[var(--v2-text-text-base)] disabled:opacity-60"
onClick={props.state.onInstall}
disabled={props.state.installing}
aria-label={props.state.ariaLabel}
title={props.state.title}
>
{props.state.label}
</button>
</Show>
)
} }
function TabNavItem(props: { function TabNavItem(props: {
@@ -600,25 +736,24 @@ function TabNavItem(props: {
title: string title: string
project?: LocalProject project?: LocalProject
directory: string directory: string
hideClose?: boolean
onClose: () => void onClose: () => void
}) { }) {
const match = useMatch(() => props.href) const match = useMatch(() => props.href)
const isActive = () => !!match() const isActive = () => !!match()
return ( return (
<div <div
class="group relative flex h-7 min-w-24 max-w-60 flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] bg-[var(--tab-bg)] pl-1.5 [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)]" class="group relative flex h-7 min-w-24 max-w-60 flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] bg-[var(--tab-bg)] px-1.5 [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)]"
data-active={isActive()} data-active={isActive()}
> >
<a <a
href={props.href} href={props.href}
class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 overflow-hidden text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base" class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 overflow-hidden text-[13px] font-medium leading-5 text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base"
> >
<ProjectTabAvatar project={props.project} directory={props.directory} /> <ProjectTabAvatar project={props.project} directory={props.directory} />
<span class="text-clip">{props.title}</span> <span class="text-clip leading-5">{props.title}</span>
</a> </a>
<div class="absolute right-0 inset-y-0 flex flex-row items-center pr-1 py-1 w-8 pl-2"> <div class="absolute not-group-hover:not-group-data-[active=true]:left-52 group-hover:right-0 group-data-[active=true]:right-0 inset-y-0 flex flex-row items-center pr-1 py-1 w-8 pl-2">
<div <div
class="absolute inset-0 bg-(image:--inactive-bg) group-hover:bg-(image:--active-bg) group-data-[active=true]:bg-(image:--active-bg)" class="absolute inset-0 bg-(image:--inactive-bg) group-hover:bg-(image:--active-bg) group-data-[active=true]:bg-(image:--active-bg)"
style={{ style={{
@@ -629,7 +764,7 @@ function TabNavItem(props: {
<IconButtonV2 <IconButtonV2
size="small" size="small"
variant="ghost-muted" variant="ghost-muted"
class="opacity-0 group-hover:opacity-100 group-data-[active='true']:opacity-100" class="opacity-0 group-hover:opacity-100 group-data-[active='true']:opacity-100 z-10"
onClick={props.onClose} onClick={props.onClose}
icon={<IconV2 name="xmark-small" />} icon={<IconV2 name="xmark-small" />}
/> />
@@ -657,12 +792,12 @@ function NewSessionTabItem(props: { href: string; title: string; onClose: () =>
<a <a
href={props.href} href={props.href}
aria-current="page" aria-current="page"
class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 overflow-hidden text-[13px] font-medium leading-none text-[var(--v2-text-text-base)]" class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 overflow-hidden text-[13px] font-medium leading-5 text-[var(--v2-text-text-base)]"
> >
<span class="flex size-4 shrink-0 rotate-90 items-center justify-center"> <span class="flex size-4 shrink-0 rotate-90 items-center justify-center">
<IconV2 name="edit" /> <IconV2 name="edit" />
</span> </span>
<span class="truncate">{props.title}</span> <span class="truncate leading-5">{props.title}</span>
</a> </a>
<div class="absolute right-0 inset-y-0 flex w-7 items-center justify-center"> <div class="absolute right-0 inset-y-0 flex w-7 items-center justify-center">
<IconButtonV2 <IconButtonV2
+55 -38
View File
@@ -8,10 +8,11 @@ import {
getSessionPrefetchPromise, getSessionPrefetchPromise,
setSessionPrefetch, setSessionPrefetch,
} from "./global-sync/session-prefetch" } from "./global-sync/session-prefetch"
import { useGlobalSync } from "./global-sync" import { createServerSyncContext } from "./server-sync"
import type { Message, OpencodeClient, Part } from "@opencode-ai/sdk/v2/client" import type { Message, Part } from "@opencode-ai/sdk/v2/client"
import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache" import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache"
import { diffs as list, message as clean } from "@/utils/diffs" import { diffs as list, message as clean } from "@/utils/diffs"
import { useServerSDK } from "./server-sdk"
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"]) const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
@@ -33,6 +34,12 @@ const keyFor = (directory: string, id: string) => `${directory}\n${id}`
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0) const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
const isNotFound = (error: unknown) =>
error instanceof Error &&
typeof error.cause === "object" &&
error.cause !== null &&
(error.cause as { status?: unknown }).status === 404
function merge<T extends { id: string }>(a: readonly T[], b: readonly T[]) { function merge<T extends { id: string }>(a: readonly T[], b: readonly T[]) {
const map = new Map(a.map((item) => [item.id, item] as const)) const map = new Map(a.map((item) => [item.id, item] as const))
for (const item of b) map.set(item.id, item) for (const item of b) map.set(item.id, item)
@@ -164,16 +171,17 @@ function setOptimisticRemove(setStore: (...args: unknown[]) => void, input: Opti
}) })
} }
export const createDirSyncContext = (client: OpencodeClient, directory: string) => { export const createDirSyncContext = (directory: string, serverSync: ReturnType<typeof createServerSyncContext>) => {
const globalSync = useGlobalSync() const serverSDK = useServerSDK()
const client = serverSDK.createClient({ directory, throwOnError: true })
type Child = ReturnType<(typeof globalSync)["child"]> type Child = ReturnType<(typeof serverSync)["child"]>
type Setter = Child[1] type Setter = Child[1]
const current = createMemo(() => globalSync.child(directory)) const current = createMemo(() => serverSync.child(directory))
const target = (directory?: string) => { const target = (directory?: string) => {
if (!directory || directory === directory) return current() if (!directory || directory === directory) return current()
return globalSync.child(directory) return serverSync.child(directory)
} }
const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/") const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
const initialMessagePageSize = 80 const initialMessagePageSize = 80
@@ -239,7 +247,7 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string)
if (!first) break if (!first) break
const stale = [...(seen.get(first) ?? [])] const stale = [...(seen.get(first) ?? [])]
seen.delete(first) seen.delete(first)
const [, setStore] = globalSync.child(first, { bootstrap: false }) const [, setStore] = serverSync.child(first, { bootstrap: false })
evict(first, setStore, stale) evict(first, setStore, stale)
} }
return created return created
@@ -267,7 +275,7 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string)
if (sessionIDs.length === 0) return if (sessionIDs.length === 0) return
clearSessionPrefetch(directory, sessionIDs) clearSessionPrefetch(directory, sessionIDs)
for (const sessionID of sessionIDs) { for (const sessionID of sessionIDs) {
globalSync.todo.set(sessionID, undefined) serverSync.todo.set(sessionID, undefined)
} }
setStore( setStore(
produce((draft) => { produce((draft) => {
@@ -324,7 +332,7 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string)
for (const messageID of next.confirmed) { for (const messageID of next.confirmed) {
clearOptimistic(input.directory, input.sessionID, messageID) clearOptimistic(input.directory, input.sessionID, messageID)
} }
const [store] = globalSync.child(input.directory, { bootstrap: false }) const [store] = serverSync.child(input.directory, { bootstrap: false })
const cached = input.mode === "prepend" ? (store.message[input.sessionID] ?? []) : [] const cached = input.mode === "prepend" ? (store.message[input.sessionID] ?? []) : []
const message = input.mode === "prepend" ? merge(cached, next.session) : next.session const message = input.mode === "prepend" ? merge(cached, next.session) : next.session
batch(() => { batch(() => {
@@ -345,6 +353,10 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string)
}) })
}) })
}) })
.catch((error) => {
if (isNotFound(error) && !tracked(input.directory, input.sessionID)) return
throw error
})
.finally(() => { .finally(() => {
setMeta( setMeta(
produce((draft) => { produce((draft) => {
@@ -373,8 +385,8 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string)
}, },
get project() { get project() {
const store = current()[0] const store = current()[0]
const match = Binary.search(globalSync.data.project, store.project, (p) => p.id) const match = Binary.search(serverSync.data.project, store.project, (p) => p.id)
if (match.found) return globalSync.data.project[match.index] if (match.found) return serverSync.data.project[match.index]
return undefined return undefined
}, },
session: { session: {
@@ -418,7 +430,7 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string)
}) })
}, },
async sync(sessionID: string, opts?: { force?: boolean }) { async sync(sessionID: string, opts?: { force?: boolean }) {
const [store, setStore] = globalSync.child(directory) const [store, setStore] = serverSync.child(directory)
const key = keyFor(directory, sessionID) const key = keyFor(directory, sessionID)
touch(directory, setStore, sessionID) touch(directory, setStore, sessionID)
@@ -456,22 +468,27 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string)
const sessionReq = const sessionReq =
hasSession && !opts?.force hasSession && !opts?.force
? Promise.resolve() ? Promise.resolve()
: retry(() => client.session.get({ sessionID })).then((session) => { : retry(() => client.session.get({ sessionID }))
if (!tracked(directory, sessionID)) return .then((session) => {
const data = session.data if (!tracked(directory, sessionID)) return
if (!data) return const data = session.data
setStore( if (!data) return
"session", setStore(
produce((draft) => { "session",
const match = Binary.search(draft, sessionID, (s) => s.id) produce((draft) => {
if (match.found) { const match = Binary.search(draft, sessionID, (s) => s.id)
draft[match.index] = data if (match.found) {
return draft[match.index] = data
} return
draft.splice(match.index, 0, data) }
}), draft.splice(match.index, 0, data)
) }),
}) )
})
.catch((error) => {
if (isNotFound(error) && !tracked(directory, sessionID)) return
throw error
})
const messagesReq = const messagesReq =
cached && !opts?.force cached && !opts?.force
@@ -488,7 +505,7 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string)
}) })
}, },
async diff(sessionID: string, opts?: { force?: boolean }) { async diff(sessionID: string, opts?: { force?: boolean }) {
const [store, setStore] = globalSync.child(directory) const [store, setStore] = serverSync.child(directory)
touch(directory, setStore, sessionID) touch(directory, setStore, sessionID)
if (store.session_diff[sessionID] !== undefined && !opts?.force) return if (store.session_diff[sessionID] !== undefined && !opts?.force) return
@@ -501,13 +518,13 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string)
) )
}, },
async todo(sessionID: string, opts?: { force?: boolean }) { async todo(sessionID: string, opts?: { force?: boolean }) {
const [store, setStore] = globalSync.child(directory) const [store, setStore] = serverSync.child(directory)
touch(directory, setStore, sessionID) touch(directory, setStore, sessionID)
const existing = store.todo[sessionID] const existing = store.todo[sessionID]
const cached = globalSync.data.session_todo[sessionID] const cached = serverSync.data.session_todo[sessionID]
if (existing !== undefined) { if (existing !== undefined) {
if (cached === undefined) { if (cached === undefined) {
globalSync.todo.set(sessionID, existing) serverSync.todo.set(sessionID, existing)
} }
if (!opts?.force) return if (!opts?.force) return
} }
@@ -522,7 +539,7 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string)
if (!tracked(directory, sessionID)) return if (!tracked(directory, sessionID)) return
const list = todo.data ?? [] const list = todo.data ?? []
setStore("todo", sessionID, reconcile(list, { key: "id" })) setStore("todo", sessionID, reconcile(list, { key: "id" }))
globalSync.todo.set(sessionID, list) serverSync.todo.set(sessionID, list)
}), }),
) )
}, },
@@ -540,7 +557,7 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string)
return meta.loading[key] ?? false return meta.loading[key] ?? false
}, },
async loadMore(sessionID: string, count?: number) { async loadMore(sessionID: string, count?: number) {
const [, setStore] = globalSync.child(directory) const [, setStore] = serverSync.child(directory)
touch(directory, setStore, sessionID) touch(directory, setStore, sessionID)
const key = keyFor(directory, sessionID) const key = keyFor(directory, sessionID)
const step = count ?? historyMessagePageSize const step = count ?? historyMessagePageSize
@@ -561,12 +578,12 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string)
}, },
}, },
evict(sessionID: string, _directory = directory) { evict(sessionID: string, _directory = directory) {
const [, setStore] = globalSync.child(_directory) const [, setStore] = serverSync.child(_directory)
seenFor(_directory).delete(sessionID) seenFor(_directory).delete(sessionID)
evict(_directory, setStore, [sessionID]) evict(_directory, setStore, [sessionID])
}, },
fetch: async (count = 10) => { fetch: async (count = 10) => {
const [store, setStore] = globalSync.child(directory) const [store, setStore] = serverSync.child(directory)
setStore("limit", (x) => x + count) setStore("limit", (x) => x + count)
await client.session.list().then((x) => { await client.session.list().then((x) => {
const sessions = (x.data ?? []) const sessions = (x.data ?? [])
@@ -578,7 +595,7 @@ export const createDirSyncContext = (client: OpencodeClient, directory: string)
}, },
more: createMemo(() => current()[0].session.length >= current()[0].limit), more: createMemo(() => current()[0].session.length >= current()[0].limit),
archive: async (sessionID: string) => { archive: async (sessionID: string) => {
const [, setStore] = globalSync.child(directory) const [, setStore] = serverSync.child(directory)
await client.session.update({ sessionID, time: { archived: Date.now() } }) await client.session.update({ sessionID, time: { archived: Date.now() } })
setStore( setStore(
produce((draft) => { produce((draft) => {
-254
View File
@@ -1,254 +0,0 @@
import type { Event } from "@opencode-ai/sdk/v2/client"
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 { createSdkForServer } from "@/utils/server"
import { useLanguage } from "./language"
import { usePlatform } from "./platform"
import { useServer } from "./server"
const isAbortError = (error: unknown) =>
error !== null && typeof error === "object" && "name" in error && error.name === "AbortError"
export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({
name: "GlobalSDK",
init: () => {
const language = useLanguage()
const server = useServer()
const platform = usePlatform()
const abort = new AbortController()
const eventFetch = (() => {
if (!platform.fetch || !server.current) return
try {
const url = new URL(server.current.http.url)
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1"
if (url.protocol === "http:" && !loopback) return platform.fetch
} catch {
return
}
})()
const currentServer = server.current
if (!currentServer) throw new Error(language.t("error.globalSDK.noServerAvailable"))
const eventSdk = createSdkForServer({
signal: abort.signal,
fetch: eventFetch,
server: currentServer.http,
})
const emitter = createGlobalEmitter<{
[key: string]: Event
}>()
type Queued = { directory: string; payload: Event }
const FLUSH_FRAME_MS = 16
const STREAM_YIELD_MS = 8
const RECONNECT_DELAY_MS = 250
let queue: Queued[] = []
let buffer: Queued[] = []
const coalesced = new Map<string, number>()
const staleDeltas = new Set<string>()
let timer: ReturnType<typeof setTimeout> | undefined
let last = 0
const deltaKey = (directory: string, messageID: string, partID: string) => `${directory}:${messageID}:${partID}`
const key = (directory: string, payload: Event) => {
if (payload.type === "session.status") return `session.status:${directory}:${payload.properties.sessionID}`
if (payload.type === "lsp.updated") return `lsp.updated:${directory}`
if (payload.type === "message.part.updated") {
const part = payload.properties.part
return `message.part.updated:${directory}:${part.messageID}:${part.id}`
}
}
const flush = () => {
if (timer) clearTimeout(timer)
timer = undefined
if (queue.length === 0) return
const events = queue
const skip = staleDeltas.size > 0 ? new Set(staleDeltas) : undefined
queue = buffer
buffer = events
queue.length = 0
coalesced.clear()
staleDeltas.clear()
last = Date.now()
batch(() => {
for (const event of events) {
if (skip && event.payload.type === "message.part.delta") {
const props = event.payload.properties
if (skip.has(deltaKey(event.directory, props.messageID, props.partID))) continue
}
emitter.emit(event.directory, event.payload)
}
})
buffer.length = 0
}
const schedule = () => {
if (timer) return
const elapsed = Date.now() - last
timer = setTimeout(flush, Math.max(0, FLUSH_FRAME_MS - elapsed))
}
let streamErrorLogged = false
const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
const aborted = isAbortError
let attempt: AbortController | undefined
let run: Promise<void> | undefined
let started = false
const HEARTBEAT_TIMEOUT_MS = 15_000
let lastEventAt = Date.now()
let heartbeat: ReturnType<typeof setTimeout> | undefined
const resetHeartbeat = () => {
lastEventAt = Date.now()
if (heartbeat) clearTimeout(heartbeat)
heartbeat = setTimeout(() => {
attempt?.abort()
}, HEARTBEAT_TIMEOUT_MS)
}
const clearHeartbeat = () => {
if (!heartbeat) return
clearTimeout(heartbeat)
heartbeat = undefined
}
const start = () => {
if (started) return run
started = true
run = (async () => {
// oxlint-disable-next-line no-unmodified-loop-condition -- `started` is set to false by stop() which also aborts; both flags are checked to allow graceful exit
while (!abort.signal.aborted && started) {
attempt = new AbortController()
lastEventAt = Date.now()
const onAbort = () => {
attempt?.abort()
}
abort.signal.addEventListener("abort", onAbort)
try {
const events = await eventSdk.global.event({
signal: attempt.signal,
onSseError: (error) => {
if (aborted(error)) return
if (streamErrorLogged) return
streamErrorLogged = true
console.error("[global-sdk] event stream error", {
url: currentServer.http.url,
fetch: eventFetch ? "platform" : "webview",
error,
})
},
})
let yielded = Date.now()
resetHeartbeat()
for await (const event of events.stream) {
resetHeartbeat()
streamErrorLogged = false
const directory = event.directory ?? "global"
if (event.payload.type === "sync") {
continue
}
const payload = event.payload as Event
const k = key(directory, payload)
if (k) {
const i = coalesced.get(k)
if (i !== undefined) {
queue[i] = { directory, payload }
if (payload.type === "message.part.updated") {
const part = payload.properties.part
staleDeltas.add(deltaKey(directory, part.messageID, part.id))
}
continue
}
coalesced.set(k, queue.length)
}
queue.push({ directory, payload })
schedule()
if (Date.now() - yielded < STREAM_YIELD_MS) continue
yielded = Date.now()
await wait(0)
}
} catch (error) {
if (!aborted(error) && !streamErrorLogged) {
streamErrorLogged = true
console.error("[global-sdk] event stream failed", {
url: currentServer.http.url,
fetch: eventFetch ? "platform" : "webview",
error,
})
}
} finally {
abort.signal.removeEventListener("abort", onAbort)
attempt = undefined
clearHeartbeat()
}
if (abort.signal.aborted || !started) return
await wait(RECONNECT_DELAY_MS)
}
})().finally(() => {
run = undefined
flush()
})
return run
}
const stop = () => {
started = false
attempt?.abort()
clearHeartbeat()
}
onMount(() => {
makeEventListener(document, "visibilitychange", () => {
if (document.visibilityState !== "visible") return
if (!started) return
if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return
attempt?.abort()
})
})
onCleanup(() => {
stop()
abort.abort()
flush()
})
const sdk = createSdkForServer({
server: server.current.http,
fetch: platform.fetch,
throwOnError: true,
})
return {
url: currentServer.http.url,
client: sdk,
event: {
on: emitter.on.bind(emitter),
listen: emitter.listen.bind(emitter),
start,
},
createClient(opts: Omit<Parameters<typeof createSdkForServer>[0], "server" | "fetch">) {
const s = server.current
if (!s) throw new Error(language.t("error.globalSDK.serverNotAvailable"))
return createSdkForServer({
server: s.http,
fetch: platform.fetch,
...opts,
})
},
}
},
})
@@ -0,0 +1,78 @@
import { describe, expect, test } from "bun:test"
import { createStore } from "solid-js/store"
import { QueryClient } from "@tanstack/solid-query"
import type { Config, OpencodeClient, Project } from "@opencode-ai/sdk/v2/client"
import type { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
import { bootstrapDirectory } from "./bootstrap"
import type { State, VcsCache } from "./types"
const provider = { all: new Map(), connected: [], default: {} } satisfies NormalizedProviderListResponse
describe("bootstrapDirectory", () => {
test("marks a loading directory partial during bootstrap and complete after success", async () => {
const [store, setStore] = createStore<State>({
status: "loading",
agent: [],
command: [],
project: "",
projectMeta: undefined,
icon: undefined,
provider_ready: true,
provider,
config: {},
path: { state: "", config: "", worktree: "/project", directory: "/project", home: "/home" },
session: [],
sessionTotal: 0,
session_status: {},
session_working(id: string) {
return this.session_status[id]?.type !== "idle"
},
session_diff: {},
todo: {},
permission: {},
question: {},
mcp_ready: true,
mcp: {},
lsp_ready: true,
lsp: [],
vcs: undefined,
limit: 5,
message: {},
part: {},
part_text_accum_delta: {},
})
await bootstrapDirectory({
directory: "/project",
global: {
config: {} satisfies Config,
path: { state: "", config: "", worktree: "/project", directory: "/project", home: "/home" },
project: [{ id: "project", worktree: "/project" } as Project],
provider,
},
sdk: {
app: { agents: async () => ({ data: [{ name: "build", mode: "primary" }] }) },
config: { get: async () => ({ data: {} }) },
session: { status: async () => ({ data: {} }) },
vcs: { get: async () => ({ data: undefined }) },
command: { list: async () => ({ data: [] }) },
permission: { list: async () => ({ data: [] }) },
question: { list: async () => ({ data: [] }) },
mcp: { status: async () => ({ data: {} }) },
provider: { list: async () => ({ data: { all: [], connected: [], default: {} } }) },
} as unknown as OpencodeClient,
store,
setStore,
vcsCache: { setStore() {} } as unknown as VcsCache,
loadSessions() {},
translate: (key) => key,
queryClient: new QueryClient(),
})
expect(store.status).toBe("partial")
await new Promise((resolve) => setTimeout(resolve, 80))
expect(store.status).toBe("complete")
})
})
@@ -5,7 +5,6 @@ import type {
PermissionRequest, PermissionRequest,
Project, Project,
ProviderAuthResponse, ProviderAuthResponse,
ProviderListResponse,
QuestionRequest, QuestionRequest,
Session, Session,
Todo, Todo,
@@ -19,7 +18,8 @@ import type { State, VcsCache } from "./types"
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils" import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
import { formatServerError } from "@/utils/server-errors" import { formatServerError } from "@/utils/server-errors"
import { QueryClient, queryOptions } from "@tanstack/solid-query" import { QueryClient, queryOptions } from "@tanstack/solid-query"
import { loadMcpQuery } from "../global-sync" import { loadMcpQuery } from "../server-sync"
import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
type GlobalStore = { type GlobalStore = {
ready: boolean ready: boolean
@@ -28,7 +28,7 @@ type GlobalStore = {
session_todo: { session_todo: {
[sessionID: string]: Todo[] [sessionID: string]: Todo[]
} }
provider: ProviderListResponse provider: NormalizedProviderListResponse
provider_auth: ProviderAuthResponse provider_auth: ProviderAuthResponse
config: Config config: Config
reload: undefined | "pending" | "complete" reload: undefined | "pending" | "complete"
@@ -105,7 +105,7 @@ export const loadProjectsQuery = (sdk: OpencodeClient) =>
}) })
export async function bootstrapGlobal(input: { export async function bootstrapGlobal(input: {
globalSDK: OpencodeClient serverSDK: OpencodeClient
requestFailedTitle: string requestFailedTitle: string
translate: (key: string, vars?: Record<string, string | number>) => string translate: (key: string, vars?: Record<string, string | number>) => string
formatMoreCount: (count: number) => string formatMoreCount: (count: number) => string
@@ -113,12 +113,12 @@ export async function bootstrapGlobal(input: {
queryClient: QueryClient queryClient: QueryClient
}) { }) {
const slow = [ const slow = [
() => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.globalSDK)), () => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.serverSDK)),
() => input.queryClient.fetchQuery(loadProvidersQuery(null, input.globalSDK)), () => input.queryClient.fetchQuery(loadProvidersQuery(null, input.serverSDK)),
() => input.queryClient.fetchQuery(loadPathQuery(null, input.globalSDK)), () => input.queryClient.fetchQuery(loadPathQuery(null, input.serverSDK)),
() => () =>
input.queryClient input.queryClient
.fetchQuery(loadProjectsQuery(input.globalSDK)) .fetchQuery(loadProjectsQuery(input.serverSDK))
.then((data) => input.setGlobalStore("project", data)), .then((data) => input.setGlobalStore("project", data)),
] ]
await runAll(slow) await runAll(slow)
@@ -208,7 +208,7 @@ export async function bootstrapDirectory(input: {
config: Config config: Config
path: Path path: Path
project: Project[] project: Project[]
provider: ProviderListResponse provider: NormalizedProviderListResponse
} }
queryClient: QueryClient queryClient: QueryClient
}) { }) {
@@ -1,10 +1,63 @@
import { describe, expect, test } from "bun:test" import { beforeAll, describe, expect, mock, test } from "bun:test"
import { createRoot, getOwner } from "solid-js" import { createRoot, getOwner, type Owner } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import type { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
import type { State } from "./types" import type { State } from "./types"
import { createChildStoreManager } from "./child-store" import type { QueryOptionsApi } from "../server-sync"
let createChildStoreManager: typeof import("./child-store").createChildStoreManager
const child = () => createStore({} as State) const child = () => createStore({} as State)
const provider = { all: new Map(), connected: [], default: {} } satisfies NormalizedProviderListResponse
const queryOptionsApi = {
globalConfig: () => ({ queryKey: ["globalConfig"], queryFn: async () => ({}) }),
projects: () => ({ queryKey: ["projects"], queryFn: async () => [] }),
providers: (directory: string | null) => ({ queryKey: [directory, "providers"], queryFn: async () => provider }),
path: (directory: string | null) => ({
queryKey: [directory, "path"],
queryFn: async () => ({
state: "",
config: "",
worktree: "",
directory: directory ?? "",
home: "",
}),
}),
agents: (directory: string) => ({ queryKey: [directory, "agents"], queryFn: async () => [] }),
mcp: (directory: string) => ({ queryKey: [directory, "mcp"], queryFn: async () => ({}) }),
lsp: (directory: string) => ({ queryKey: [directory, "lsp"], queryFn: async () => [] }),
sessions: (directory: string) => ({ queryKey: [directory, "loadSessions"] as const }),
} as unknown as QueryOptionsApi
function createOwner(callback: (owner: Owner) => void) {
return createRoot((dispose) => {
const owner = getOwner()
if (!owner) throw new Error("owner required")
callback(owner)
return dispose
})
}
beforeAll(async () => {
mock.module("@/utils/persist", () => ({
Persist: {
workspace: (...parts: string[]) => parts.join(":"),
},
persisted: (_target: string, store: unknown[]) => [store[0], store[1], null, () => true],
}))
mock.module("@tanstack/solid-query", () => ({
useQueries: () => [
{ isLoading: false, data: { state: "", config: "", worktree: "", directory: "", home: "" } },
{ isLoading: false, data: {} },
{ isLoading: false, data: [] },
{ isLoading: false, data: provider },
],
}))
createChildStoreManager = (await import("./child-store")).createChildStoreManager
})
describe("createChildStoreManager", () => { describe("createChildStoreManager", () => {
test("does not evict the active directory during mark", () => { test("does not evict the active directory during mark", () => {
@@ -22,8 +75,8 @@ describe("createChildStoreManager", () => {
onBootstrap() {}, onBootstrap() {},
onDispose() {}, onDispose() {},
translate: (key) => key, translate: (key) => key,
queryOptions: {} as any, queryOptions: queryOptionsApi,
global: { provider: null! }, global: { provider },
}) })
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => { Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {
@@ -37,4 +90,35 @@ describe("createChildStoreManager", () => {
expect(manager.children[directory]).toBeDefined() expect(manager.children[directory]).toBeDefined()
}) })
test("starts new child stores as loading and bootstraps them on first access", () => {
const bootstraps: string[] = []
let manager: ReturnType<typeof createChildStoreManager> | undefined
const dispose = createOwner((owner) => {
manager = createChildStoreManager({
owner,
isBooting: () => false,
isLoadingSessions: () => false,
onBootstrap(directory) {
bootstraps.push(directory)
},
onDispose() {},
translate: (key) => key,
queryOptions: queryOptionsApi,
global: { provider },
})
})
try {
if (!manager) throw new Error("manager required")
const [store] = manager.child("/project")
expect(store.status).toBe("loading")
expect(bootstraps).toEqual(["/project"])
} finally {
dispose()
}
})
}) })
@@ -1,7 +1,7 @@
import { createRoot, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js" import { createRoot, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js"
import { createStore, type SetStoreFunction, type Store } from "solid-js/store" import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
import { Persist, persisted } from "@/utils/persist" import { Persist, persisted } from "@/utils/persist"
import type { ProviderListResponse, VcsInfo } from "@opencode-ai/sdk/v2/client" import type { VcsInfo } from "@opencode-ai/sdk/v2/client"
import { import {
DIR_IDLE_TTL_MS, DIR_IDLE_TTL_MS,
MAX_DIR_STORES, MAX_DIR_STORES,
@@ -15,8 +15,9 @@ import {
} from "./types" } from "./types"
import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction" import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
import { useQueries } from "@tanstack/solid-query" import { useQueries } from "@tanstack/solid-query"
import { QueryOptionsApi } from "../global-sync" import { QueryOptionsApi } from "../server-sync"
import { directoryKey, type DirectoryKey } from "./utils" import { directoryKey, type DirectoryKey } from "./utils"
import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
export function createChildStoreManager(input: { export function createChildStoreManager(input: {
owner: Owner owner: Owner
@@ -27,7 +28,7 @@ export function createChildStoreManager(input: {
translate: (key: string, vars?: Record<string, string | number>) => string translate: (key: string, vars?: Record<string, string | number>) => string
queryOptions: QueryOptionsApi queryOptions: QueryOptionsApi
global: { global: {
provider: ProviderListResponse provider: NormalizedProviderListResponse
} }
}) { }) {
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {} const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
@@ -190,10 +191,9 @@ export function createChildStoreManager(input: {
return !providerQuery.isLoading return !providerQuery.isLoading
}, },
get provider() { get provider() {
const EMPTY = { all: [], connected: [], default: {} } const EMPTY = { all: new Map(), connected: [], default: {} }
if (providerQuery.isLoading) return EMPTY if (providerQuery.isLoading) return EMPTY
if (providerQuery.data?.all.length === 0 && input.global.provider.all.length > 0) if (providerQuery.data?.all.size === 0 && input.global.provider.all.size > 0) return input.global.provider
return input.global.provider
return providerQuery.data ?? EMPTY return providerQuery.data ?? EMPTY
}, },
config: {}, config: {},
@@ -8,7 +8,6 @@ import type {
Part, Part,
Path, Path,
PermissionRequest, PermissionRequest,
ProviderListResponse,
QuestionRequest, QuestionRequest,
Session, Session,
SessionStatus, SessionStatus,
@@ -16,6 +15,7 @@ import type {
Todo, Todo,
VcsInfo, VcsInfo,
} from "@opencode-ai/sdk/v2/client" } from "@opencode-ai/sdk/v2/client"
import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
import type { Accessor } from "solid-js" import type { Accessor } from "solid-js"
import type { SetStoreFunction, Store } from "solid-js/store" import type { SetStoreFunction, Store } from "solid-js/store"
@@ -38,7 +38,7 @@ export type State = {
projectMeta: ProjectMeta | undefined projectMeta: ProjectMeta | undefined
icon: string | undefined icon: string | undefined
provider_ready: boolean provider_ready: boolean
provider: ProviderListResponse provider: NormalizedProviderListResponse
config: Config config: Config
path: Path path: Path
session: Session[] session: Session[]
+16 -5
View File
@@ -1,4 +1,5 @@
import type { Agent, Project, ProviderListResponse } from "@opencode-ai/sdk/v2/client" import type { Agent, Project, ProviderListResponse } from "@opencode-ai/sdk/v2/client"
import { NormalizedProviderListResponse } from "@opencode-ai/ui/context"
export { pathKey as directoryKey, type PathKey as DirectoryKey } from "@/utils/path-key" export { pathKey as directoryKey, type PathKey as DirectoryKey } from "@/utils/path-key"
export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0) export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
@@ -17,13 +18,23 @@ export function normalizeAgentList(input: unknown): Agent[] {
return Object.values(input).filter(isAgent) return Object.values(input).filter(isAgent)
} }
export function normalizeProviderList(input: ProviderListResponse): ProviderListResponse { export function normalizeProviderList(input: ProviderListResponse): NormalizedProviderListResponse {
return { return {
...input, ...input,
all: input.all.map((provider) => ({ all: new Map(
...provider, input.all.map(
models: Object.fromEntries(Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated")), (provider) =>
})), [
provider.id,
{
...provider,
models: Object.fromEntries(
Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated"),
),
},
] as const,
),
),
} }
} }
+13 -13
View File
@@ -2,8 +2,8 @@ import { createStore, produce } from "solid-js/store"
import { batch, createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js" import { batch, createEffect, createMemo, onCleanup, onMount, type Accessor } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context" import { createSimpleContext } from "@opencode-ai/ui/context"
import { makeEventListener } from "@solid-primitives/event-listener" import { makeEventListener } from "@solid-primitives/event-listener"
import { useGlobalSync } from "./global-sync" import { useServerSync } from "./server-sync"
import { useGlobalSDK } from "./global-sdk" import { useServerSDK } from "./server-sdk"
import { useServer } from "./server" import { useServer } from "./server"
import { usePlatform } from "./platform" import { usePlatform } from "./platform"
import { Project } from "@opencode-ai/sdk/v2" import { Project } from "@opencode-ai/sdk/v2"
@@ -136,8 +136,8 @@ const normalizeStoredSessionTabs = (key: string, tabs: SessionTabs) => {
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
name: "Layout", name: "Layout",
init: () => { init: () => {
const globalSdk = useGlobalSDK() const globalSdk = useServerSDK()
const globalSync = useGlobalSync() const serverSync = useServerSync()
const server = useServer() const server = useServer()
const platform = usePlatform() const platform = usePlatform()
@@ -386,11 +386,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
} }
function enrich(project: { worktree: string; expanded: boolean }) { function enrich(project: { worktree: string; expanded: boolean }) {
const [childStore] = globalSync.child(project.worktree, { bootstrap: false }) const [childStore] = serverSync.child(project.worktree, { bootstrap: false })
const projectID = childStore.project const projectID = childStore.project
const metadata = projectID const metadata = projectID
? globalSync.data.project.find((x) => x.id === projectID) ? serverSync.data.project.find((x) => x.id === projectID)
: globalSync.data.project.find((x) => x.worktree === project.worktree) : serverSync.data.project.find((x) => x.worktree === project.worktree)
// Preserve local icon override from per-workspace localStorage cache (childStore.icon). // Preserve local icon override from per-workspace localStorage cache (childStore.icon).
// Without this, different subdirectories of the same git repo would share the same // Without this, different subdirectories of the same git repo would share the same
@@ -404,7 +404,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
const roots = createMemo(() => { const roots = createMemo(() => {
const map = new Map<string, string>() const map = new Map<string, string>()
for (const project of globalSync.data.project) { for (const project of serverSync.data.project) {
const sandboxes = project.sandboxes ?? [] const sandboxes = project.sandboxes ?? []
for (const sandbox of sandboxes) { for (const sandbox of sandboxes) {
map.set(sandbox, project.worktree) map.set(sandbox, project.worktree)
@@ -470,12 +470,12 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
createEffect(() => { createEffect(() => {
const projects = enriched() const projects = enriched()
if (projects.length === 0) return if (projects.length === 0) return
if (!globalSync.ready) return if (!serverSync.ready) return
for (const project of projects) { for (const project of projects) {
if (!project.id) continue if (!project.id) continue
if (project.id === "global") continue if (project.id === "global") continue
globalSync.project.icon(project.worktree, project.icon?.override) serverSync.project.icon(project.worktree, project.icon?.override)
} }
}) })
@@ -509,7 +509,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
colorRequested.set(worktree, color) colorRequested.set(worktree, color)
if (project.id === "global") { if (project.id === "global") {
globalSync.project.meta(worktree, { icon: { color } }) serverSync.project.meta(worktree, { icon: { color } })
continue continue
} }
@@ -531,7 +531,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
sessionTimer = undefined sessionTimer = undefined
void Promise.all( void Promise.all(
server.projects.list().map((project) => { server.projects.list().map((project) => {
return globalSync.project.loadSessions(project.worktree) return serverSync.project.loadSessions(project.worktree)
}), }),
) )
}, 0) }, 0)
@@ -560,7 +560,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
open(directory: string) { open(directory: string) {
const root = rootFor(directory) const root = rootFor(directory)
if (server.projects.list().find((x) => x.worktree === root)) return if (server.projects.list().find((x) => x.worktree === root)) return
void globalSync.project.loadSessions(root) void serverSync.project.loadSessions(root)
server.projects.open(root) server.projects.open(root)
}, },
close(directory: string) { close(directory: string) {
+1 -1
View File
@@ -90,7 +90,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}) })
const validModel = (model: ModelKey) => { const validModel = (model: ModelKey) => {
const provider = providers.all().find((item) => item.id === model.providerID) const provider = providers.all().get(model.providerID)
return !!provider?.models[model.modelID] && connected().has(model.providerID) return !!provider?.models[model.modelID] && connected().has(model.providerID)
} }
+7 -7
View File
@@ -2,8 +2,8 @@ import { createStore, reconcile } from "solid-js/store"
import { batch, createEffect, createMemo, onCleanup } from "solid-js" import { batch, createEffect, createMemo, onCleanup } from "solid-js"
import { useParams } from "@solidjs/router" import { useParams } from "@solidjs/router"
import { createSimpleContext } from "@opencode-ai/ui/context" import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSDK } from "./global-sdk" import { useServerSDK } from "./server-sdk"
import { useGlobalSync } from "./global-sync" import { useServerSync } from "./server-sync"
import { usePlatform } from "@/context/platform" import { usePlatform } from "@/context/platform"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings" import { useSettings } from "@/context/settings"
@@ -109,8 +109,8 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
name: "Notification", name: "Notification",
init: () => { init: () => {
const params = useParams() const params = useParams()
const globalSDK = useGlobalSDK() const serverSDK = useServerSDK()
const globalSync = useGlobalSync() const serverSync = useServerSync()
const platform = usePlatform() const platform = usePlatform()
const settings = useSettings() const settings = useSettings()
const language = useLanguage() const language = useLanguage()
@@ -207,10 +207,10 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
const lookup = async (directory: string, sessionID?: string) => { const lookup = async (directory: string, sessionID?: string) => {
if (!sessionID) return undefined if (!sessionID) return undefined
const [syncStore] = globalSync.child(directory, { bootstrap: false }) const [syncStore] = serverSync.child(directory, { bootstrap: false })
const match = Binary.search(syncStore.session, sessionID, (s) => s.id) const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
if (match.found) return syncStore.session[match.index] if (match.found) return syncStore.session[match.index]
return globalSDK.client.session return serverSDK.client.session
.get({ directory, sessionID }) .get({ directory, sessionID })
.then((x) => x.data) .then((x) => x.data)
.catch(() => undefined) .catch(() => undefined)
@@ -285,7 +285,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
}) })
} }
const unsub = globalSDK.event.listen((e) => { const unsub = serverSDK.event.listen((e) => {
const event = e.details const event = e.details
if (event.type !== "session.idle" && event.type !== "session.error") return if (event.type !== "session.idle" && event.type !== "session.error") return
+13 -13
View File
@@ -3,8 +3,8 @@ import { createStore, produce } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context" import { createSimpleContext } from "@opencode-ai/ui/context"
import type { PermissionRequest } from "@opencode-ai/sdk/v2/client" import type { PermissionRequest } from "@opencode-ai/sdk/v2/client"
import { Persist, persisted } from "@/utils/persist" import { Persist, persisted } from "@/utils/persist"
import { useGlobalSDK } from "@/context/global-sdk" import { useServerSDK } from "@/context/server-sdk"
import { useGlobalSync } from "./global-sync" import { useServerSync } from "./server-sync"
import { useParams } from "@solidjs/router" import { useParams } from "@solidjs/router"
import { decode64 } from "@/utils/base64" import { decode64 } from "@/utils/base64"
import { import {
@@ -48,13 +48,13 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
name: "Permission", name: "Permission",
init: () => { init: () => {
const params = useParams() const params = useParams()
const globalSDK = useGlobalSDK() const serverSDK = useServerSDK()
const globalSync = useGlobalSync() const serverSync = useServerSync()
const permissionsEnabled = createMemo(() => { const permissionsEnabled = createMemo(() => {
const directory = decode64(params.dir) const directory = decode64(params.dir)
if (!directory) return false if (!directory) return false
const [store] = globalSync.child(directory) const [store] = serverSync.child(directory)
return hasPermissionPromptRules(store.config.permission) return hasPermissionPromptRules(store.config.permission)
}) })
@@ -86,7 +86,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
if (!ready()) return if (!ready()) return
const directory = decode64(params.dir) const directory = decode64(params.dir)
if (!directory) return if (!directory) return
const [childStore] = globalSync.child(directory) const [childStore] = serverSync.child(directory)
const perm = childStore.config.permission const perm = childStore.config.permission
if (typeof perm === "string" && perm === "allow") { if (typeof perm === "string" && perm === "allow") {
const key = directoryAcceptKey(directory) const key = directoryAcceptKey(directory)
@@ -118,7 +118,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
} }
const respond: PermissionRespondFn = (input) => { const respond: PermissionRespondFn = (input) => {
globalSDK.client.permission.respond(input).catch(() => { serverSDK.client.permission.respond(input).catch(() => {
responded.delete(input.permissionID) responded.delete(input.permissionID)
}) })
} }
@@ -139,7 +139,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
} }
function isAutoAccepting(sessionID: string, directory?: string) { function isAutoAccepting(sessionID: string, directory?: string) {
const session = directory ? globalSync.child(directory, { bootstrap: false })[0].session : [] const session = directory ? serverSync.child(directory, { bootstrap: false })[0].session : []
return autoRespondsPermission(store.autoAccept, session, { sessionID }, directory) return autoRespondsPermission(store.autoAccept, session, { sessionID }, directory)
} }
@@ -148,7 +148,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
} }
function shouldAutoRespond(permission: PermissionRequest, directory?: string) { function shouldAutoRespond(permission: PermissionRequest, directory?: string) {
const session = directory ? globalSync.child(directory, { bootstrap: false })[0].session : [] const session = directory ? serverSync.child(directory, { bootstrap: false })[0].session : []
return autoRespondsPermission(store.autoAccept, session, permission, directory) return autoRespondsPermission(store.autoAccept, session, permission, directory)
} }
@@ -159,7 +159,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
return next return next
} }
const unsubscribe = globalSDK.event.listen((e) => { const unsubscribe = serverSDK.event.listen((e) => {
const event = e.details const event = e.details
if (event?.type !== "permission.asked") return if (event?.type !== "permission.asked") return
@@ -178,7 +178,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
}), }),
) )
globalSDK.client.permission serverSDK.client.permission
.list({ directory }) .list({ directory })
.then((x) => { .then((x) => {
if (!isAutoAcceptingDirectory(directory)) return if (!isAutoAcceptingDirectory(directory)) return
@@ -210,7 +210,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
}), }),
) )
globalSDK.client.permission serverSDK.client.permission
.list({ directory }) .list({ directory })
.then((x) => { .then((x) => {
if (enableVersion.get(key) !== version) return if (enableVersion.get(key) !== version) return
@@ -268,7 +268,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
}, },
permissionsEnabled, permissionsEnabled,
isPermissionAllowAll(directory: string) { isPermissionAllowAll(directory: string) {
const [childStore] = globalSync.child(directory) const [childStore] = serverSync.child(directory)
const perm = childStore.config.permission const perm = childStore.config.permission
return typeof perm === "string" && perm === "allow" return typeof perm === "string" && perm === "allow"
}, },
+4 -42
View File
@@ -1,49 +1,11 @@
import type { Event } from "@opencode-ai/sdk/v2/client"
import { createSimpleContext } from "@opencode-ai/ui/context" import { createSimpleContext } from "@opencode-ai/ui/context"
import { createGlobalEmitter } from "@solid-primitives/event-bus" import { useServerSDK } from "./server-sdk"
import { type Accessor, createEffect, createMemo, onCleanup } from "solid-js"
import { useGlobalSDK } from "./global-sdk"
type SDKEventMap = {
[key in Event["type"]]: Extract<Event, { type: key }>
}
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
name: "SDK", name: "SDK",
init: (props: { directory: Accessor<string> }) => { init: (props: { directory: string }) => {
const globalSDK = useGlobalSDK() const serverSDK = useServerSDK()
const directory = createMemo(props.directory) return serverSDK.createDirSdkContext(props.directory)
const client = createMemo(() =>
globalSDK.createClient({
directory: directory(),
throwOnError: true,
}),
)
const emitter = createGlobalEmitter<SDKEventMap>()
createEffect(() => {
const unsub = globalSDK.event.on(directory(), (event) => {
emitter.emit(event.type, event)
})
onCleanup(unsub)
})
return {
get directory() {
return directory()
},
get client() {
return client()
},
event: emitter,
get url() {
return globalSDK.url
},
createClient(opts: Parameters<typeof globalSDK.createClient>[0]) {
return globalSDK.createClient(opts)
},
}
}, },
}) })
+290
View File
@@ -0,0 +1,290 @@
import type { Event } from "@opencode-ai/sdk/v2/client"
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 { createSdkForServer } from "@/utils/server"
import { useLanguage } from "./language"
import { usePlatform } from "./platform"
import { ServerConnection, useServer } from "./server"
import { createRefCountMap } from "@/utils/refcount"
const isAbortError = (error: unknown) =>
error !== null && typeof error === "object" && "name" in error && error.name === "AbortError"
function createServerSdkContext(server: ServerConnection.Any) {
const platform = usePlatform()
const abort = new AbortController()
const eventFetch = (() => {
if (!platform.fetch || !server) return
try {
const url = new URL(server.http.url)
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1"
if (url.protocol === "http:" && !loopback) return platform.fetch
} catch {
return
}
})()
const eventSdk = createSdkForServer({
signal: abort.signal,
fetch: eventFetch,
server: server.http,
})
const emitter = createGlobalEmitter<{
[key: string]: Event
}>()
type Queued = { directory: string; payload: Event }
const FLUSH_FRAME_MS = 16
const STREAM_YIELD_MS = 8
const RECONNECT_DELAY_MS = 250
let queue: Queued[] = []
let buffer: Queued[] = []
const coalesced = new Map<string, number>()
const staleDeltas = new Set<string>()
let timer: ReturnType<typeof setTimeout> | undefined
let last = 0
const deltaKey = (directory: string, messageID: string, partID: string) => `${directory}:${messageID}:${partID}`
const key = (directory: string, payload: Event) => {
if (payload.type === "session.status") return `session.status:${directory}:${payload.properties.sessionID}`
if (payload.type === "lsp.updated") return `lsp.updated:${directory}`
if (payload.type === "message.part.updated") {
const part = payload.properties.part
return `message.part.updated:${directory}:${part.messageID}:${part.id}`
}
}
const flush = () => {
if (timer) clearTimeout(timer)
timer = undefined
if (queue.length === 0) return
const events = queue
const skip = staleDeltas.size > 0 ? new Set(staleDeltas) : undefined
queue = buffer
buffer = events
queue.length = 0
coalesced.clear()
staleDeltas.clear()
last = Date.now()
batch(() => {
for (const event of events) {
if (skip && event.payload.type === "message.part.delta") {
const props = event.payload.properties
if (skip.has(deltaKey(event.directory, props.messageID, props.partID))) continue
}
emitter.emit(event.directory, event.payload)
}
})
buffer.length = 0
}
const schedule = () => {
if (timer) return
const elapsed = Date.now() - last
timer = setTimeout(flush, Math.max(0, FLUSH_FRAME_MS - elapsed))
}
let streamErrorLogged = false
const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
const aborted = isAbortError
let attempt: AbortController | undefined
let run: Promise<void> | undefined
let started = false
const HEARTBEAT_TIMEOUT_MS = 15_000
let lastEventAt = Date.now()
let heartbeat: ReturnType<typeof setTimeout> | undefined
const resetHeartbeat = () => {
lastEventAt = Date.now()
if (heartbeat) clearTimeout(heartbeat)
heartbeat = setTimeout(() => {
attempt?.abort()
}, HEARTBEAT_TIMEOUT_MS)
}
const clearHeartbeat = () => {
if (!heartbeat) return
clearTimeout(heartbeat)
heartbeat = undefined
}
const start = () => {
if (started) return run
started = true
run = (async () => {
// oxlint-disable-next-line no-unmodified-loop-condition -- `started` is set to false by stop() which also aborts; both flags are checked to allow graceful exit
while (!abort.signal.aborted && started) {
attempt = new AbortController()
lastEventAt = Date.now()
const onAbort = () => {
attempt?.abort()
}
abort.signal.addEventListener("abort", onAbort)
try {
const events = await eventSdk.global.event({
signal: attempt.signal,
onSseError: (error) => {
if (aborted(error)) return
if (streamErrorLogged) return
streamErrorLogged = true
console.error("[global-sdk] event stream error", {
url: server.http.url,
fetch: eventFetch ? "platform" : "webview",
error,
})
},
})
let yielded = Date.now()
resetHeartbeat()
for await (const event of events.stream) {
resetHeartbeat()
streamErrorLogged = false
const directory = event.directory ?? "global"
if (event.payload.type === "sync") {
continue
}
const payload = event.payload as Event
const k = key(directory, payload)
if (k) {
const i = coalesced.get(k)
if (i !== undefined) {
queue[i] = { directory, payload }
if (payload.type === "message.part.updated") {
const part = payload.properties.part
staleDeltas.add(deltaKey(directory, part.messageID, part.id))
}
continue
}
coalesced.set(k, queue.length)
}
queue.push({ directory, payload })
schedule()
if (Date.now() - yielded < STREAM_YIELD_MS) continue
yielded = Date.now()
await wait(0)
}
} catch (error) {
if (!aborted(error) && !streamErrorLogged) {
streamErrorLogged = true
console.error("[global-sdk] event stream failed", {
url: server.http.url,
fetch: eventFetch ? "platform" : "webview",
error,
})
}
} finally {
abort.signal.removeEventListener("abort", onAbort)
attempt = undefined
clearHeartbeat()
}
if (abort.signal.aborted || !started) return
await wait(RECONNECT_DELAY_MS)
}
})().finally(() => {
run = undefined
flush()
})
return run
}
const stop = () => {
started = false
attempt?.abort()
clearHeartbeat()
}
onMount(() => {
makeEventListener(document, "visibilitychange", () => {
if (document.visibilityState !== "visible") return
if (!started) return
if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return
attempt?.abort()
})
})
onCleanup(() => {
stop()
abort.abort()
flush()
})
const sdk = createSdkForServer({
server: server.http,
fetch: platform.fetch,
throwOnError: true,
})
return {
url: server.http.url,
client: sdk,
event: {
on: emitter.on.bind(emitter),
listen: emitter.listen.bind(emitter),
start,
},
createClient(opts: Omit<Parameters<typeof createSdkForServer>[0], "server" | "fetch">) {
return createSdkForServer({
server: server.http,
fetch: platform.fetch,
...opts,
})
},
}
}
export const { use: useServerSDK, provider: ServerSDKProvider } = createSimpleContext({
name: "ServerSDK",
init: () => {
const language = useLanguage()
const server = useServer()
if (!server.current) throw new Error(language.t("error.serverSDK.noServerAvailable"))
const sdk = createServerSdkContext(server.current)
return {
...sdk,
createDirSdkContext: createRefCountMap((dir) => createDirSdkContext(dir, sdk)),
}
},
})
type SDKEventMap = {
[key in Event["type"]]: Extract<Event, { type: key }>
}
function createDirSdkContext(directory: string, serverSDK: ReturnType<typeof createServerSdkContext>) {
const client = serverSDK.createClient({
directory,
throwOnError: true,
})
const emitter = createGlobalEmitter<SDKEventMap>()
const unsub = serverSDK.event.on(directory, (event) => {
emitter.emit(event.type, event)
})
onCleanup(unsub)
return {
directory,
client,
event: emitter,
get url() {
return serverSDK.url
},
createClient(opts: Parameters<typeof serverSDK.createClient>[0]) {
return serverSDK.createClient(opts)
},
}
}
@@ -1,19 +1,11 @@
import type { import type { Config, OpencodeClient, Path, Project, ProviderAuthResponse, Todo } from "@opencode-ai/sdk/v2/client"
Config,
OpencodeClient,
Path,
Project,
ProviderAuthResponse,
ProviderListResponse,
Todo,
} from "@opencode-ai/sdk/v2/client"
import { showToast } from "@opencode-ai/ui/toast" import { showToast } from "@opencode-ai/ui/toast"
import { getFilename } from "@opencode-ai/core/util/path" import { getFilename } from "@opencode-ai/core/util/path"
import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js" import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
import { createStore, produce, reconcile } from "solid-js/store" import { createStore, produce, reconcile } from "solid-js/store"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import type { InitError } from "../pages/error" import type { InitError } from "../pages/error"
import { useGlobalSDK } from "./global-sdk" import { useServerSDK } from "./server-sdk"
import { import {
bootstrapDirectory, bootstrapDirectory,
bootstrapGlobal, bootstrapGlobal,
@@ -37,6 +29,8 @@ import { createRefreshQueue } from "./global-sync/queue"
import { directoryKey } from "./global-sync/utils" import { directoryKey } from "./global-sync/utils"
import { PathKey } from "@/utils/path-key" import { PathKey } from "@/utils/path-key"
import { createDirSyncContext } from "./directory-sync" import { createDirSyncContext } from "./directory-sync"
import { createSimpleContext, NormalizedProviderListResponse } from "@opencode-ai/ui/context"
import { createRefCountMap } from "@/utils/refcount"
type GlobalStore = { type GlobalStore = {
ready: boolean ready: boolean
@@ -46,7 +40,7 @@ type GlobalStore = {
session_todo: { session_todo: {
[sessionID: string]: Todo[] [sessionID: string]: Todo[]
} }
provider: ProviderListResponse provider: NormalizedProviderListResponse
provider_auth: ProviderAuthResponse provider_auth: ProviderAuthResponse
config: Config config: Config
reload: undefined | "pending" | "complete" reload: undefined | "pending" | "complete"
@@ -64,13 +58,13 @@ export const loadLspQuery = (directory: string, sdk: OpencodeClient) =>
queryFn: () => sdk.lsp.status().then((r) => r.data ?? []), queryFn: () => sdk.lsp.status().then((r) => r.data ?? []),
}) })
function makeQueryOptionsApi(globalSDK: () => OpencodeClient, sdkFor: (dir: PathKey) => OpencodeClient) { function makeQueryOptionsApi(serverSDK: () => OpencodeClient, sdkFor: (dir: PathKey) => OpencodeClient) {
return { return {
globalConfig: () => loadGlobalConfigQuery(globalSDK()), globalConfig: () => loadGlobalConfigQuery(serverSDK()),
projects: () => loadProjectsQuery(globalSDK()), projects: () => loadProjectsQuery(serverSDK()),
providers: (directory: PathKey | null) => providers: (directory: PathKey | null) =>
loadProvidersQuery(directory, directory === null ? globalSDK() : sdkFor(directory)), loadProvidersQuery(directory, directory === null ? serverSDK() : sdkFor(directory)),
path: (directory: PathKey | null) => loadPathQuery(directory, directory === null ? globalSDK() : sdkFor(directory)), path: (directory: PathKey | null) => loadPathQuery(directory, directory === null ? serverSDK() : sdkFor(directory)),
agents: (directory: PathKey) => loadAgentsQuery(directory, sdkFor(directory)), agents: (directory: PathKey) => loadAgentsQuery(directory, sdkFor(directory)),
mcp: (directory: PathKey) => loadMcpQuery(directory, sdkFor(directory)), mcp: (directory: PathKey) => loadMcpQuery(directory, sdkFor(directory)),
lsp: (directory: PathKey) => loadLspQuery(directory, sdkFor(directory)), lsp: (directory: PathKey) => loadLspQuery(directory, sdkFor(directory)),
@@ -79,11 +73,11 @@ function makeQueryOptionsApi(globalSDK: () => OpencodeClient, sdkFor: (dir: Path
} }
export type QueryOptionsApi = ReturnType<typeof makeQueryOptionsApi> export type QueryOptionsApi = ReturnType<typeof makeQueryOptionsApi>
function createGlobalSync() { export function createServerSyncContext() {
const globalSDK = useGlobalSDK() const serverSDK = useServerSDK()
const language = useLanguage() const language = useLanguage()
const owner = getOwner() const owner = getOwner()
if (!owner) throw new Error("GlobalSync must be created within owner") if (!owner) throw new Error("ServerSync must be created within owner")
const sdkCache = new Map<string, OpencodeClient>() const sdkCache = new Map<string, OpencodeClient>()
const booting = new Map<string, Promise<void>>() const booting = new Map<string, Promise<void>>()
@@ -94,7 +88,7 @@ function createGlobalSync() {
const key = directoryKey(directory) const key = directoryKey(directory)
const cached = sdkCache.get(key) const cached = sdkCache.get(key)
if (cached) return cached if (cached) return cached
const sdk = globalSDK.createClient({ const sdk = serverSDK.createClient({
directory, directory,
throwOnError: true, throwOnError: true,
}) })
@@ -102,7 +96,7 @@ function createGlobalSync() {
return sdk return sdk
} }
const queryOptionsApi = makeQueryOptionsApi(() => globalSDK.client, sdkFor) const queryOptionsApi = makeQueryOptionsApi(() => serverSDK.client, sdkFor)
const [configQuery, providerQuery, pathQuery] = useQueries(() => ({ const [configQuery, providerQuery, pathQuery] = useQueries(() => ({
queries: [queryOptionsApi.globalConfig(), queryOptionsApi.providers(null), queryOptionsApi.path(null)], queries: [queryOptionsApi.globalConfig(), queryOptionsApi.providers(null), queryOptionsApi.path(null)],
@@ -121,7 +115,7 @@ function createGlobalSync() {
return pathQuery.data ?? EMPTY return pathQuery.data ?? EMPTY
}, },
get provider() { get provider() {
const EMPTY = { all: [], connected: [], default: {} } const EMPTY = { all: new Map(), connected: [], default: {} }
if (providerQuery.isLoading) return EMPTY if (providerQuery.isLoading) return EMPTY
return providerQuery.data ?? EMPTY return providerQuery.data ?? EMPTY
}, },
@@ -161,7 +155,7 @@ function createGlobalSync() {
queryKey: ["bootstrap"], queryKey: ["bootstrap"],
queryFn: async () => { queryFn: async () => {
await bootstrapGlobal({ await bootstrapGlobal({
globalSDK: globalSDK.client, serverSDK: serverSDK.client,
requestFailedTitle: language.t("common.requestFailed"), requestFailedTitle: language.t("common.requestFailed"),
translate: language.t, translate: language.t,
formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }), formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }),
@@ -255,7 +249,7 @@ function createGlobalSync() {
loadRootSessionsWithFallback({ loadRootSessionsWithFallback({
directory, directory,
limit, limit,
list: (query) => globalSDK.client.session.list(query), list: (query) => serverSDK.client.session.list(query),
}) })
.then((x) => { .then((x) => {
const nonArchived = (x.data ?? []) const nonArchived = (x.data ?? [])
@@ -341,7 +335,7 @@ function createGlobalSync() {
return promise return promise
} }
const unsub = globalSDK.event.listen((e) => { const unsub = serverSDK.event.listen((e) => {
const directory = e.name const directory = e.name
const key = directoryKey(directory) const key = directoryKey(directory)
const event = e.details const event = e.details
@@ -400,13 +394,13 @@ function createGlobalSync() {
eventFrame = undefined eventFrame = undefined
eventTimer = setTimeout(() => { eventTimer = setTimeout(() => {
eventTimer = undefined eventTimer = undefined
void globalSDK.event.start() void serverSDK.event.start()
}, 0) }, 0)
}) })
} else { } else {
eventTimer = setTimeout(() => { eventTimer = setTimeout(() => {
eventTimer = undefined eventTimer = undefined
void globalSDK.event.start() void serverSDK.event.start()
}, 0) }, 0)
} }
}) })
@@ -422,7 +416,7 @@ function createGlobalSync() {
} }
const updateConfigMutation = useMutation(() => ({ const updateConfigMutation = useMutation(() => ({
mutationFn: (config: Config) => globalSDK.client.global.config.update({ config }), mutationFn: (config: Config) => serverSDK.client.global.config.update({ config }),
onSuccess: () => { onSuccess: () => {
bootstrap.refetch() bootstrap.refetch()
// Invalidate all provider queries so newly configured custom providers // Invalidate all provider queries so newly configured custom providers
@@ -432,9 +426,6 @@ function createGlobalSync() {
}, },
})) }))
const dirSyncContexts = new Map<string, ReturnType<typeof createDirSyncContext>>()
const dirSyncContextRefCounts = new Map<string, number>()
return { return {
data: globalStore, data: globalStore,
set, set,
@@ -453,42 +444,21 @@ function createGlobalSync() {
todo: { todo: {
set: setSessionTodo, set: setSessionTodo,
}, },
createDirSyncContext: (directory: string) => {
onCleanup(() => {
dirSyncContextRefCounts.set(directory, (dirSyncContextRefCounts.get(directory) ?? 0) - 1)
if (dirSyncContextRefCounts.get(directory) === 0) {
dirSyncContexts.delete(directory)
dirSyncContextRefCounts.delete(directory)
}
})
const cached = dirSyncContexts.get(directory)
if (cached) {
dirSyncContextRefCounts.set(directory, (dirSyncContextRefCounts.get(directory) ?? 0) + 1)
return cached
}
const ctx = createDirSyncContext(globalSDK.createClient({ directory, throwOnError: true }), directory)
dirSyncContexts.set(directory, ctx)
dirSyncContextRefCounts.set(directory, 1)
return ctx
},
} }
} }
const GlobalSyncContext = createContext<ReturnType<typeof createGlobalSync>>() export const { use: useServerSync, provider: ServerSyncProvider } = createSimpleContext({
name: "ServerSync",
init: () => {
const sync = createServerSyncContext()
export function GlobalSyncProvider(props: ParentProps) { return {
const value = createGlobalSync() ...sync,
return <GlobalSyncContext.Provider value={value}>{props.children}</GlobalSyncContext.Provider> createDirSyncContext: createRefCountMap((dir) => createDirSyncContext(dir, sync)),
} }
},
export function useGlobalSync() { })
const context = useContext(GlobalSyncContext)
if (!context) throw new Error("useGlobalSync must be used within GlobalSyncProvider")
return context
}
export function useQueryOptions() { export function useQueryOptions() {
return useGlobalSync().queryOptions return useServerSync().queryOptions
} }
+20 -64
View File
@@ -1,8 +1,7 @@
import { createSimpleContext } from "@opencode-ai/ui/context" import { createSimpleContext } from "@opencode-ai/ui/context"
import { type Accessor, batch, createEffect, createMemo, onCleanup } from "solid-js" import { type Accessor, batch, createMemo } from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { Persist, persisted } from "@/utils/persist" import { Persist, persisted } from "@/utils/persist"
import { useCheckServerHealth } from "@/utils/server-health"
type StoredProject = { worktree: string; expanded: boolean } type StoredProject = { worktree: string; expanded: boolean }
type StoredServer = string | ServerConnection.HttpBase | ServerConnection.Http type StoredServer = string | ServerConnection.HttpBase | ServerConnection.Http
@@ -37,24 +36,30 @@ export function resolveServerList(input: {
props?: Array<ServerConnection.Any> props?: Array<ServerConnection.Any>
stored: StoredServer[] stored: StoredServer[]
}): Array<ServerConnection.Any> { }): Array<ServerConnection.Any> {
const servers = [ const deduped = new Map<ServerConnection.Key, ServerConnection.Any>(
...input.stored.map((value) => input.props?.map((v) => [ServerConnection.key(v), v]) ?? [],
)
for (const value of input.stored) {
const conn: ServerConnection.Http =
typeof value === "string" typeof value === "string"
? { ? {
type: "http" as const, type: "http" as const,
http: { url: value }, http: { url: value },
} }
: value, : "http" in value
), ? value
...(input.props ?? []), : { type: "http", http: value }
]
const deduped = new Map<ServerConnection.Key, ServerConnection.Any>()
for (const value of servers) {
const conn: ServerConnection.Any = "type" in value ? value : { type: "http", http: value }
const key = ServerConnection.key(conn) const key = ServerConnection.key(conn)
if (deduped.has(key) && conn.type === "http" && !conn.authToken) continue
deduped.set(key, conn) const existing = deduped.get(key)
if (existing)
deduped.set(key, {
...existing,
...conn,
http: { ...existing.http, ...conn.http },
})
else deduped.set(key, conn)
} }
return [...deduped.values()] return [...deduped.values()]
@@ -122,13 +127,7 @@ export namespace ServerConnection {
export const { use: useServer, provider: ServerProvider } = createSimpleContext({ export const { use: useServer, provider: ServerProvider } = createSimpleContext({
name: "Server", name: "Server",
init: (props: { init: (props: { defaultServer: ServerConnection.Key; servers?: Array<ServerConnection.Any> }) => {
defaultServer: ServerConnection.Key
disableHealthCheck?: boolean
servers?: Array<ServerConnection.Any>
}) => {
const checkServerHealth = useCheckServerHealth()
const [store, setStore, _, ready] = persisted( const [store, setStore, _, ready] = persisted(
Persist.global("server", ["server.v3"]), Persist.global("server", ["server.v3"]),
createStore({ createStore({
@@ -146,36 +145,8 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
const [state, setState] = createStore({ const [state, setState] = createStore({
active: props.defaultServer, active: props.defaultServer,
healthy: undefined as boolean | undefined,
}) })
const healthy = () => state.healthy
function startHealthPolling(conn: ServerConnection.Any) {
let alive = true
let busy = false
const run = () => {
if (busy) return
busy = true
void check(conn)
.then((next) => {
if (!alive) return
setState("healthy", next)
})
.finally(() => {
busy = false
})
}
run()
const interval = setInterval(run, HEALTH_POLL_INTERVAL_MS)
return () => {
alive = false
clearInterval(interval)
}
}
function setActive(input: ServerConnection.Key) { function setActive(input: ServerConnection.Key) {
if (state.active !== input) setState("active", input) if (state.active !== input) setState("active", input)
} }
@@ -209,20 +180,6 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
const isReady = createMemo(() => ready() && !!state.active) const isReady = createMemo(() => ready() && !!state.active)
const check = (conn: ServerConnection.Any) => checkServerHealth(conn.http).then((x) => x.healthy)
createEffect(() => {
const current_ = current()
if (!current_) return
if (props.disableHealthCheck) {
setState("healthy", true)
return
}
setState("healthy", undefined)
onCleanup(startHealthPolling(current_))
})
const origin = createMemo(() => projectsKey(state.active)) const origin = createMemo(() => projectsKey(state.active))
const projectsList = createMemo(() => store.projects[origin()] ?? []) const projectsList = createMemo(() => store.projects[origin()] ?? [])
const current: Accessor<ServerConnection.Any | undefined> = createMemo( const current: Accessor<ServerConnection.Any | undefined> = createMemo(
@@ -235,7 +192,6 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
return { return {
ready: isReady, ready: isReady,
healthy,
isLocal, isLocal,
get key() { get key() {
return state.active return state.active
+20
View File
@@ -0,0 +1,20 @@
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useServer } from "./server"
import { useServerHealth } from "@/utils/server-health"
export const { use: useServers, provider: ServersProvider } = createSimpleContext({
name: "Servers",
init: () => {
const server = useServer()
const health = useServerHealth(
() => server.list,
() => true,
)
return {
list: () => server.list,
health,
}
},
})
+12
View File
@@ -32,6 +32,8 @@ export interface Settings {
shellToolPartsExpanded: boolean shellToolPartsExpanded: boolean
editToolPartsExpanded: boolean editToolPartsExpanded: boolean
showSessionProgressBar: boolean showSessionProgressBar: boolean
showCustomAgents: boolean
newLayoutDesigns?: boolean
} }
updates: { updates: {
startup: boolean startup: boolean
@@ -53,6 +55,7 @@ export interface Settings {
export const monoDefault = "System Mono" export const monoDefault = "System Mono"
export const sansDefault = "System Sans" export const sansDefault = "System Sans"
export const terminalDefault = "JetBrainsMono Nerd Font Mono" export const terminalDefault = "JetBrainsMono Nerd Font Mono"
export const newLayoutDesignsDefault = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
const monoFallback = const monoFallback =
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
@@ -117,6 +120,7 @@ const defaultSettings: Settings = {
shellToolPartsExpanded: false, shellToolPartsExpanded: false,
editToolPartsExpanded: false, editToolPartsExpanded: false,
showSessionProgressBar: true, showSessionProgressBar: true,
showCustomAgents: false,
}, },
updates: { updates: {
startup: true, startup: true,
@@ -236,6 +240,14 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
setShowSessionProgressBar(value: boolean) { setShowSessionProgressBar(value: boolean) {
setStore("general", "showSessionProgressBar", value) setStore("general", "showSessionProgressBar", value)
}, },
showCustomAgents: withFallback(() => store.general?.showCustomAgents, defaultSettings.general.showCustomAgents),
setShowCustomAgents(value: boolean) {
setStore("general", "showCustomAgents", value)
},
newLayoutDesigns: withFallback(() => store.general?.newLayoutDesigns, newLayoutDesignsDefault),
setNewLayoutDesigns(value: boolean) {
setStore("general", "newLayoutDesigns", value)
},
}, },
updates: { updates: {
startup: withFallback(() => store.updates?.startup, defaultSettings.updates.startup), startup: withFallback(() => store.updates?.startup, defaultSettings.updates.startup),
+6 -56
View File
@@ -1,6 +1,5 @@
import { Binary } from "@opencode-ai/core/util/binary" import { Binary } from "@opencode-ai/core/util/binary"
import { createSimpleContext } from "@opencode-ai/ui/context" import { useServerSync } from "./server-sync"
import { useGlobalSync } from "./global-sync"
import { useSDK } from "./sdk" import { useSDK } from "./sdk"
import type { Message, Part } from "@opencode-ai/sdk/v2/client" import type { Message, Part } from "@opencode-ai/sdk/v2/client"
@@ -10,26 +9,8 @@ function sortParts(parts: Part[]) {
return parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id)) return parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id))
} }
function runInflight(map: Map<string, Promise<void>>, key: string, task: () => Promise<void>) {
const pending = map.get(key)
if (pending) return pending
const promise = task().finally(() => {
map.delete(key)
})
map.set(key, promise)
return promise
}
const keyFor = (directory: string, id: string) => `${directory}\n${id}`
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0) const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0)
function merge<T extends { id: string }>(a: readonly T[], b: readonly T[]) {
const map = new Map(a.map((item) => [item.id, item] as const))
for (const item of b) map.set(item.id, item)
return [...map.values()].sort((x, y) => cmp(x.id, y.id))
}
type OptimisticStore = { type OptimisticStore = {
message: Record<string, Message[] | undefined> message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined> part: Record<string, Part[] | undefined>
@@ -127,40 +108,9 @@ export function applyOptimisticRemove(draft: OptimisticStore, input: OptimisticR
delete draft.part[input.messageID] delete draft.part[input.messageID]
} }
function setOptimisticAdd(setStore: (...args: unknown[]) => void, input: OptimisticAddInput) { export const useSync = () => {
setStore("message", input.sessionID, (messages: Message[] | undefined) => { const serverSync = useServerSync()
if (!messages) return [input.message] const sdk = useSDK()
const result = Binary.search(messages, input.message.id, (m) => m.id)
const next = [...messages] return serverSync.createDirSyncContext(sdk.directory)
next.splice(result.index, 0, input.message)
return next
})
setStore("part", input.message.id, sortParts(input.parts))
} }
function setOptimisticRemove(setStore: (...args: unknown[]) => void, input: OptimisticRemoveInput) {
setStore("message", input.sessionID, (messages: Message[] | undefined) => {
if (!messages) return messages
const result = Binary.search(messages, input.messageID, (m) => m.id)
if (!result.found) return messages
const next = [...messages]
next.splice(result.index, 1)
return next
})
setStore("part", (part: Record<string, Part[] | undefined>) => {
if (!(input.messageID in part)) return part
const next = { ...part }
delete next[input.messageID]
return next
})
}
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
name: "Sync",
init: () => {
const globalSync = useGlobalSync()
const sdk = useSDK()
return globalSync.createDirSyncContext(sdk.directory)
},
})
+26 -9
View File
@@ -1,6 +1,7 @@
import { useGlobalSync } from "@/context/global-sync" import { useServerSync } from "@/context/server-sync"
import { decode64 } from "@/utils/base64" import { decode64 } from "@/utils/base64"
import { useParams } from "@solidjs/router" import { useParams } from "@solidjs/router"
import { Iterable, pipe } from "effect"
import { createMemo } from "solid-js" import { createMemo } from "solid-js"
export const popularProviders = [ export const popularProviders = [
@@ -16,29 +17,45 @@ export const popularProviders = [
const popularProviderSet = new Set(popularProviders) const popularProviderSet = new Set(popularProviders)
export function useProviders() { export function useProviders() {
const globalSync = useGlobalSync() const serverSync = useServerSync()
const params = useParams() const params = useParams()
const dir = createMemo(() => decode64(params.dir) ?? "") const dir = createMemo(() => decode64(params.dir) ?? "")
const providers = () => { const providers = () => {
if (dir()) { if (dir()) {
const [projectStore] = globalSync.child(dir()) const [projectStore] = serverSync.child(dir())
if (projectStore.provider_ready) return projectStore.provider if (projectStore.provider_ready) return projectStore.provider
} }
return globalSync.data.provider return serverSync.data.provider
} }
return { return {
all: () => providers().all, all: () => providers().all,
default: () => providers().default, default: () => providers().default,
popular: () => providers().all.filter((p) => popularProviderSet.has(p.id)), popular: () =>
pipe(
providers().all,
Iterable.map(([, p]) => p),
Iterable.filter((p) => popularProviderSet.has(p.id)),
(v) => Array.from(v),
),
connected: () => { connected: () => {
const connected = new Set(providers().connected) const connected = new Set(providers().connected)
return providers().all.filter((p) => connected.has(p.id)) return pipe(
providers().all,
Iterable.map(([, p]) => p),
Iterable.filter((p) => connected.has(p.id)),
(v) => Array.from(v),
)
}, },
paid: () => { paid: () => {
const connected = new Set(providers().connected) const connected = new Set(providers().connected)
return providers().all.filter( return [
(p) => connected.has(p.id) && (p.id !== "opencode" || Object.values(p.models).some((m) => m.cost?.input)), ...Iterable.filter(
) providers().all,
([id]) =>
connected.has(id) &&
(id !== "opencode" || Object.values(providers().all.get(id)?.models ?? {}).some((m) => m.cost?.input)),
),
]
}, },
} }
} }
+3 -3
View File
@@ -413,7 +413,7 @@ export const dict = {
"error.page.version": "الإصدار: {{version}}", "error.page.version": "الإصدار: {{version}}",
"error.dev.rootNotFound": "error.dev.rootNotFound":
"لم يتم العثور على العنصر الجذري. هل نسيت إضافته إلى index.html؟ أو ربما تمت كتابة سمة id بشكل خاطئ؟", "لم يتم العثور على العنصر الجذري. هل نسيت إضافته إلى index.html؟ أو ربما تمت كتابة سمة id بشكل خاطئ؟",
"error.globalSync.connectFailed": "تعذر الاتصال بالخادم. هل هناك خادم يعمل في `{{url}}`؟", "error.serverSync.connectFailed": "تعذر الاتصال بالخادم. هل هناك خادم يعمل في `{{url}}`؟",
"directory.error.invalidUrl": "دليل غير صالح في عنوان URL.", "directory.error.invalidUrl": "دليل غير صالح في عنوان URL.",
"error.chain.unknown": "خطأ غير معروف", "error.chain.unknown": "خطأ غير معروف",
"error.chain.causedBy": "بسبب:", "error.chain.causedBy": "بسبب:",
@@ -837,8 +837,8 @@ export const dict = {
"common.key.insert": "Insert", "common.key.insert": "Insert",
"common.unknown": "غير معروف", "common.unknown": "غير معروف",
"error.page.circular": "[دائري]", "error.page.circular": "[دائري]",
"error.globalSDK.noServerAvailable": "لا يوجد خادم متاح", "error.serverSDK.noServerAvailable": "لا يوجد خادم متاح",
"error.globalSDK.serverNotAvailable": "الخادم غير متاح", "error.serverSDK.serverNotAvailable": "الخادم غير متاح",
"error.childStore.persistedCacheCreateFailed": "فشل إنشاء ذاكرة التخزين المؤقت الدائمة", "error.childStore.persistedCacheCreateFailed": "فشل إنشاء ذاكرة التخزين المؤقت الدائمة",
"error.childStore.persistedProjectMetadataCreateFailed": "فشل إنشاء بيانات تعريف المشروع الدائمة", "error.childStore.persistedProjectMetadataCreateFailed": "فشل إنشاء بيانات تعريف المشروع الدائمة",
"error.childStore.persistedProjectIconCreateFailed": "فشل إنشاء أيقونة المشروع الدائمة", "error.childStore.persistedProjectIconCreateFailed": "فشل إنشاء أيقونة المشروع الدائمة",
+3 -3
View File
@@ -414,7 +414,7 @@ export const dict = {
"error.page.version": "Versão: {{version}}", "error.page.version": "Versão: {{version}}",
"error.dev.rootNotFound": "error.dev.rootNotFound":
"Elemento raiz não encontrado. Você esqueceu de adicioná-lo ao seu index.html? Ou talvez o atributo id foi escrito incorretamente?", "Elemento raiz não encontrado. Você esqueceu de adicioná-lo ao seu index.html? Ou talvez o atributo id foi escrito incorretamente?",
"error.globalSync.connectFailed": "Não foi possível conectar ao servidor. Há um servidor executando em `{{url}}`?", "error.serverSync.connectFailed": "Não foi possível conectar ao servidor. Há um servidor executando em `{{url}}`?",
"directory.error.invalidUrl": "Diretório inválido na URL.", "directory.error.invalidUrl": "Diretório inválido na URL.",
"error.chain.unknown": "Erro desconhecido", "error.chain.unknown": "Erro desconhecido",
"error.chain.causedBy": "Causado por:", "error.chain.causedBy": "Causado por:",
@@ -850,8 +850,8 @@ export const dict = {
"common.key.insert": "Insert", "common.key.insert": "Insert",
"common.unknown": "desconhecido", "common.unknown": "desconhecido",
"error.page.circular": "[Circular]", "error.page.circular": "[Circular]",
"error.globalSDK.noServerAvailable": "Nenhum servidor disponível", "error.serverSDK.noServerAvailable": "Nenhum servidor disponível",
"error.globalSDK.serverNotAvailable": "Servidor indisponível", "error.serverSDK.serverNotAvailable": "Servidor indisponível",
"error.childStore.persistedCacheCreateFailed": "Falha ao criar cache persistente", "error.childStore.persistedCacheCreateFailed": "Falha ao criar cache persistente",
"error.childStore.persistedProjectMetadataCreateFailed": "Falha ao criar metadados de projeto persistentes", "error.childStore.persistedProjectMetadataCreateFailed": "Falha ao criar metadados de projeto persistentes",
"error.childStore.persistedProjectIconCreateFailed": "Falha ao criar ícone de projeto persistente", "error.childStore.persistedProjectIconCreateFailed": "Falha ao criar ícone de projeto persistente",
+3 -3
View File
@@ -462,7 +462,7 @@ export const dict = {
"error.dev.rootNotFound": "error.dev.rootNotFound":
"Korijenski element nije pronađen. Da li si zaboravio da ga dodaš u index.html? Ili je možda id atribut pogrešno napisan?", "Korijenski element nije pronađen. Da li si zaboravio da ga dodaš u index.html? Ili je možda id atribut pogrešno napisan?",
"error.globalSync.connectFailed": "Nije moguće povezati se na server. Da li server radi na `{{url}}`?", "error.serverSync.connectFailed": "Nije moguće povezati se na server. Da li server radi na `{{url}}`?",
"directory.error.invalidUrl": "Nevažeći direktorij u URL-u.", "directory.error.invalidUrl": "Nevažeći direktorij u URL-u.",
"error.chain.unknown": "Nepoznata greška", "error.chain.unknown": "Nepoznata greška",
@@ -926,8 +926,8 @@ export const dict = {
"common.key.insert": "Insert", "common.key.insert": "Insert",
"common.unknown": "nepoznato", "common.unknown": "nepoznato",
"error.page.circular": "[Kružno]", "error.page.circular": "[Kružno]",
"error.globalSDK.noServerAvailable": "Nema dostupnog servera", "error.serverSDK.noServerAvailable": "Nema dostupnog servera",
"error.globalSDK.serverNotAvailable": "Server nije dostupan", "error.serverSDK.serverNotAvailable": "Server nije dostupan",
"error.childStore.persistedCacheCreateFailed": "Nije uspjelo kreiranje trajnog keša", "error.childStore.persistedCacheCreateFailed": "Nije uspjelo kreiranje trajnog keša",
"error.childStore.persistedProjectMetadataCreateFailed": "Nije uspjelo kreiranje trajnih metapodataka projekta", "error.childStore.persistedProjectMetadataCreateFailed": "Nije uspjelo kreiranje trajnih metapodataka projekta",
"error.childStore.persistedProjectIconCreateFailed": "Nije uspjelo kreiranje trajne ikone projekta", "error.childStore.persistedProjectIconCreateFailed": "Nije uspjelo kreiranje trajne ikone projekta",
+3 -3
View File
@@ -459,7 +459,7 @@ export const dict = {
"error.dev.rootNotFound": "error.dev.rootNotFound":
"Rodelement ikke fundet. Har du glemt at tilføje det til din index.html? Eller måske er id-attributten stavet forkert?", "Rodelement ikke fundet. Har du glemt at tilføje det til din index.html? Eller måske er id-attributten stavet forkert?",
"error.globalSync.connectFailed": "Kunne ikke forbinde til server. Kører der en server på `{{url}}`?", "error.serverSync.connectFailed": "Kunne ikke forbinde til server. Kører der en server på `{{url}}`?",
"directory.error.invalidUrl": "Ugyldig mappe i URL.", "directory.error.invalidUrl": "Ugyldig mappe i URL.",
"error.chain.unknown": "Ukendt fejl", "error.chain.unknown": "Ukendt fejl",
@@ -920,8 +920,8 @@ export const dict = {
"common.key.insert": "Insert", "common.key.insert": "Insert",
"common.unknown": "ukendt", "common.unknown": "ukendt",
"error.page.circular": "[Cirkulær]", "error.page.circular": "[Cirkulær]",
"error.globalSDK.noServerAvailable": "Ingen server tilgængelig", "error.serverSDK.noServerAvailable": "Ingen server tilgængelig",
"error.globalSDK.serverNotAvailable": "Server ikke tilgængelig", "error.serverSDK.serverNotAvailable": "Server ikke tilgængelig",
"error.childStore.persistedCacheCreateFailed": "Kunne ikke oprette vedvarende cache", "error.childStore.persistedCacheCreateFailed": "Kunne ikke oprette vedvarende cache",
"error.childStore.persistedProjectMetadataCreateFailed": "Kunne ikke oprette vedvarende projektmetadata", "error.childStore.persistedProjectMetadataCreateFailed": "Kunne ikke oprette vedvarende projektmetadata",
"error.childStore.persistedProjectIconCreateFailed": "Kunne ikke oprette vedvarende projektikon", "error.childStore.persistedProjectIconCreateFailed": "Kunne ikke oprette vedvarende projektikon",
+3 -3
View File
@@ -421,7 +421,7 @@ export const dict = {
"error.page.version": "Version: {{version}}", "error.page.version": "Version: {{version}}",
"error.dev.rootNotFound": "error.dev.rootNotFound":
"Wurzelelement nicht gefunden. Haben Sie vergessen, es in Ihre index.html aufzunehmen? Oder wurde das id-Attribut falsch geschrieben?", "Wurzelelement nicht gefunden. Haben Sie vergessen, es in Ihre index.html aufzunehmen? Oder wurde das id-Attribut falsch geschrieben?",
"error.globalSync.connectFailed": "Verbindung zum Server fehlgeschlagen. Läuft ein Server unter `{{url}}`?", "error.serverSync.connectFailed": "Verbindung zum Server fehlgeschlagen. Läuft ein Server unter `{{url}}`?",
"directory.error.invalidUrl": "Ungültiges Verzeichnis in der URL.", "directory.error.invalidUrl": "Ungültiges Verzeichnis in der URL.",
"error.chain.unknown": "Unbekannter Fehler", "error.chain.unknown": "Unbekannter Fehler",
"error.chain.causedBy": "Verursacht durch:", "error.chain.causedBy": "Verursacht durch:",
@@ -862,8 +862,8 @@ export const dict = {
"common.key.insert": "Einfg", "common.key.insert": "Einfg",
"common.unknown": "unbekannt", "common.unknown": "unbekannt",
"error.page.circular": "[Zirkulär]", "error.page.circular": "[Zirkulär]",
"error.globalSDK.noServerAvailable": "Kein Server verfügbar", "error.serverSDK.noServerAvailable": "Kein Server verfügbar",
"error.globalSDK.serverNotAvailable": "Server nicht verfügbar", "error.serverSDK.serverNotAvailable": "Server nicht verfügbar",
"error.childStore.persistedCacheCreateFailed": "Dauerhafter Cache konnte nicht erstellt werden", "error.childStore.persistedCacheCreateFailed": "Dauerhafter Cache konnte nicht erstellt werden",
"error.childStore.persistedProjectMetadataCreateFailed": "Dauerhafte Projektmetadaten konnten nicht erstellt werden", "error.childStore.persistedProjectMetadataCreateFailed": "Dauerhafte Projektmetadaten konnten nicht erstellt werden",
"error.childStore.persistedProjectIconCreateFailed": "Dauerhaftes Projekticon konnte nicht erstellt werden", "error.childStore.persistedProjectIconCreateFailed": "Dauerhaftes Projekticon konnte nicht erstellt werden",
+13 -4
View File
@@ -221,6 +221,7 @@ export const dict = {
"common.loading": "Loading", "common.loading": "Loading",
"common.loading.ellipsis": "...", "common.loading.ellipsis": "...",
"common.cancel": "Cancel", "common.cancel": "Cancel",
"common.clear": "Clear",
"common.open": "Open", "common.open": "Open",
"common.connect": "Connect", "common.connect": "Connect",
"common.disconnect": "Disconnect", "common.disconnect": "Disconnect",
@@ -482,9 +483,9 @@ export const dict = {
"error.dev.rootNotFound": "error.dev.rootNotFound":
"Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?", "Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
"error.globalSync.connectFailed": "Could not connect to server. Is there a server running at `{{url}}`?", "error.serverSync.connectFailed": "Could not connect to server. Is there a server running at `{{url}}`?",
"error.globalSDK.noServerAvailable": "No server available", "error.serverSDK.noServerAvailable": "No server available",
"error.globalSDK.serverNotAvailable": "Server not available", "error.serverSDK.serverNotAvailable": "Server not available",
"error.childStore.persistedCacheCreateFailed": "Failed to create persisted cache", "error.childStore.persistedCacheCreateFailed": "Failed to create persisted cache",
"error.childStore.persistedProjectMetadataCreateFailed": "Failed to create persisted project metadata", "error.childStore.persistedProjectMetadataCreateFailed": "Failed to create persisted project metadata",
"error.childStore.persistedProjectIconCreateFailed": "Failed to create persisted project icon", "error.childStore.persistedProjectIconCreateFailed": "Failed to create persisted project icon",
@@ -531,6 +532,7 @@ export const dict = {
"home.project.add": "Add project", "home.project.add": "Add project",
"home.sessions.search.placeholder": "Search sessions", "home.sessions.search.placeholder": "Search sessions",
"home.sessions.empty": "No sessions found", "home.sessions.empty": "No sessions found",
"home.sessions.empty.description": "Start a new session for this project",
"home.sessions.group.today": "Today", "home.sessions.group.today": "Today",
"home.sessions.group.yesterday": "Yesterday", "home.sessions.group.yesterday": "Yesterday",
"home.sessions.group.older": "Older", "home.sessions.group.older": "Older",
@@ -584,6 +586,9 @@ export const dict = {
"session.revertDock.restore": "Restore message", "session.revertDock.restore": "Restore message",
"session.new.title": "Build anything", "session.new.title": "Build anything",
"session.new.project.new": "New project",
"session.new.project.search": "Search projects",
"session.new.project.add": "Add project",
"session.new.worktree.main": "Main branch", "session.new.worktree.main": "Main branch",
"session.new.worktree.mainWithBranch": "Main branch ({{branch}})", "session.new.worktree.mainWithBranch": "Main branch ({{branch}})",
"session.new.worktree.create": "Create new worktree", "session.new.worktree.create": "Create new worktree",
@@ -764,7 +769,7 @@ export const dict = {
"settings.general.row.followup.option.queue": "Queue", "settings.general.row.followup.option.queue": "Queue",
"settings.general.row.followup.option.steer": "Steer", "settings.general.row.followup.option.steer": "Steer",
"settings.general.row.showFileTree.title": "File tree", "settings.general.row.showFileTree.title": "File tree",
"settings.general.row.showFileTree.description": "Show the file tree toggle and panel in desktop sessions", "settings.general.row.showFileTree.description": "Show the file tree panel in desktop sessions",
"settings.general.row.showNavigation.title": "Navigation controls", "settings.general.row.showNavigation.title": "Navigation controls",
"settings.general.row.showNavigation.description": "Show the back and forward buttons in the desktop title bar", "settings.general.row.showNavigation.description": "Show the back and forward buttons in the desktop title bar",
"settings.general.row.showSearch.title": "Command palette", "settings.general.row.showSearch.title": "Command palette",
@@ -773,6 +778,8 @@ export const dict = {
"settings.general.row.showTerminal.description": "Show the terminal button in the desktop title bar", "settings.general.row.showTerminal.description": "Show the terminal button in the desktop title bar",
"settings.general.row.showStatus.title": "Server status", "settings.general.row.showStatus.title": "Server status",
"settings.general.row.showStatus.description": "Show the server status button in the desktop title bar", "settings.general.row.showStatus.description": "Show the server status button in the desktop title bar",
"settings.general.row.showCustomAgents.title": "Custom agents",
"settings.general.row.showCustomAgents.description": "Show the agent picker in the v2 desktop composer",
"settings.general.row.reasoningSummaries.title": "Show reasoning summaries", "settings.general.row.reasoningSummaries.title": "Show reasoning summaries",
"settings.general.row.reasoningSummaries.description": "Display model reasoning summaries in the timeline", "settings.general.row.reasoningSummaries.description": "Display model reasoning summaries in the timeline",
"settings.general.row.shellToolPartsExpanded.title": "Expand shell tool parts", "settings.general.row.shellToolPartsExpanded.title": "Expand shell tool parts",
@@ -784,6 +791,8 @@ export const dict = {
"settings.general.row.showSessionProgressBar.title": "Show session progress bar", "settings.general.row.showSessionProgressBar.title": "Show session progress bar",
"settings.general.row.showSessionProgressBar.description": "settings.general.row.showSessionProgressBar.description":
"Display the animated progress bar at the top of the session when the agent is working", "Display the animated progress bar at the top of the session when the agent is working",
"settings.general.row.newLayoutDesigns.title": "New layout and designs",
"settings.general.row.newLayoutDesigns.description": "Enable the redesigned layout, home, composer, and session UI",
"settings.general.row.pinchZoom.title": "Pinch to zoom", "settings.general.row.pinchZoom.title": "Pinch to zoom",
"settings.general.row.pinchZoom.description": "Allow trackpad pinch and Ctrl-scroll gestures to zoom", "settings.general.row.pinchZoom.description": "Allow trackpad pinch and Ctrl-scroll gestures to zoom",
+3 -3
View File
@@ -462,7 +462,7 @@ export const dict = {
"error.dev.rootNotFound": "error.dev.rootNotFound":
"Elemento raíz no encontrado. ¿Olvidaste añadirlo a tu index.html? ¿O tal vez el atributo id está mal escrito?", "Elemento raíz no encontrado. ¿Olvidaste añadirlo a tu index.html? ¿O tal vez el atributo id está mal escrito?",
"error.globalSync.connectFailed": "No se pudo conectar al servidor. ¿Hay un servidor ejecutándose en `{{url}}`?", "error.serverSync.connectFailed": "No se pudo conectar al servidor. ¿Hay un servidor ejecutándose en `{{url}}`?",
"directory.error.invalidUrl": "URL de directorio inválida.", "directory.error.invalidUrl": "URL de directorio inválida.",
"error.chain.unknown": "Error desconocido", "error.chain.unknown": "Error desconocido",
@@ -933,8 +933,8 @@ export const dict = {
"common.key.insert": "Insert", "common.key.insert": "Insert",
"common.unknown": "desconocido", "common.unknown": "desconocido",
"error.page.circular": "[Circular]", "error.page.circular": "[Circular]",
"error.globalSDK.noServerAvailable": "Ningún servidor disponible", "error.serverSDK.noServerAvailable": "Ningún servidor disponible",
"error.globalSDK.serverNotAvailable": "Servidor no disponible", "error.serverSDK.serverNotAvailable": "Servidor no disponible",
"error.childStore.persistedCacheCreateFailed": "Error al crear caché persistente", "error.childStore.persistedCacheCreateFailed": "Error al crear caché persistente",
"error.childStore.persistedProjectMetadataCreateFailed": "Error al crear metadatos de proyecto persistentes", "error.childStore.persistedProjectMetadataCreateFailed": "Error al crear metadatos de proyecto persistentes",
"error.childStore.persistedProjectIconCreateFailed": "Error al crear icono de proyecto persistente", "error.childStore.persistedProjectIconCreateFailed": "Error al crear icono de proyecto persistente",
+3 -3
View File
@@ -417,7 +417,7 @@ export const dict = {
"error.page.version": "Version : {{version}}", "error.page.version": "Version : {{version}}",
"error.dev.rootNotFound": "error.dev.rootNotFound":
"Élément racine introuvable. Avez-vous oublié de l'ajouter à votre index.html ? Ou peut-être que l'attribut id est mal orthographié ?", "Élément racine introuvable. Avez-vous oublié de l'ajouter à votre index.html ? Ou peut-être que l'attribut id est mal orthographié ?",
"error.globalSync.connectFailed": "error.serverSync.connectFailed":
"Impossible de se connecter au serveur. Y a-t-il un serveur en cours d'exécution à `{{url}}` ?", "Impossible de se connecter au serveur. Y a-t-il un serveur en cours d'exécution à `{{url}}` ?",
"directory.error.invalidUrl": "Répertoire invalide dans l'URL.", "directory.error.invalidUrl": "Répertoire invalide dans l'URL.",
"error.chain.unknown": "Erreur inconnue", "error.chain.unknown": "Erreur inconnue",
@@ -860,8 +860,8 @@ export const dict = {
"common.key.insert": "Inser", "common.key.insert": "Inser",
"common.unknown": "inconnu", "common.unknown": "inconnu",
"error.page.circular": "[Circulaire]", "error.page.circular": "[Circulaire]",
"error.globalSDK.noServerAvailable": "Aucun serveur disponible", "error.serverSDK.noServerAvailable": "Aucun serveur disponible",
"error.globalSDK.serverNotAvailable": "Serveur non disponible", "error.serverSDK.serverNotAvailable": "Serveur non disponible",
"error.childStore.persistedCacheCreateFailed": "Échec de la création du cache persistant", "error.childStore.persistedCacheCreateFailed": "Échec de la création du cache persistant",
"error.childStore.persistedProjectMetadataCreateFailed": "error.childStore.persistedProjectMetadataCreateFailed":
"Échec de la création des métadonnées de projet persistantes", "Échec de la création des métadonnées de projet persistantes",
+3 -3
View File
@@ -413,7 +413,7 @@ export const dict = {
"error.page.version": "バージョン: {{version}}", "error.page.version": "バージョン: {{version}}",
"error.dev.rootNotFound": "error.dev.rootNotFound":
"ルート要素が見つかりません。index.htmlに追加するのを忘れていませんか?またはid属性のスペルが間違っていませんか?", "ルート要素が見つかりません。index.htmlに追加するのを忘れていませんか?またはid属性のスペルが間違っていませんか?",
"error.globalSync.connectFailed": "サーバーに接続できませんでした。`{{url}}`でサーバーが実行されていますか?", "error.serverSync.connectFailed": "サーバーに接続できませんでした。`{{url}}`でサーバーが実行されていますか?",
"directory.error.invalidUrl": "URL内のディレクトリが無効です。", "directory.error.invalidUrl": "URL内のディレクトリが無効です。",
"error.chain.unknown": "不明なエラー", "error.chain.unknown": "不明なエラー",
"error.chain.causedBy": "原因:", "error.chain.causedBy": "原因:",
@@ -844,8 +844,8 @@ export const dict = {
"common.key.insert": "Insert", "common.key.insert": "Insert",
"common.unknown": "不明", "common.unknown": "不明",
"error.page.circular": "[循環]", "error.page.circular": "[循環]",
"error.globalSDK.noServerAvailable": "利用可能なサーバーがありません", "error.serverSDK.noServerAvailable": "利用可能なサーバーがありません",
"error.globalSDK.serverNotAvailable": "サーバーが利用できません", "error.serverSDK.serverNotAvailable": "サーバーが利用できません",
"error.childStore.persistedCacheCreateFailed": "永続キャッシュの作成に失敗しました", "error.childStore.persistedCacheCreateFailed": "永続キャッシュの作成に失敗しました",
"error.childStore.persistedProjectMetadataCreateFailed": "永続プロジェクトメタデータの作成に失敗しました", "error.childStore.persistedProjectMetadataCreateFailed": "永続プロジェクトメタデータの作成に失敗しました",
"error.childStore.persistedProjectIconCreateFailed": "永続プロジェクトアイコンの作成に失敗しました", "error.childStore.persistedProjectIconCreateFailed": "永続プロジェクトアイコンの作成に失敗しました",
+3 -3
View File
@@ -412,7 +412,7 @@ export const dict = {
"error.page.version": "버전: {{version}}", "error.page.version": "버전: {{version}}",
"error.dev.rootNotFound": "error.dev.rootNotFound":
"루트 요소를 찾을 수 없습니다. index.html에 추가하는 것을 잊으셨나요? 또는 id 속성의 철자가 틀렸을 수 있습니다.", "루트 요소를 찾을 수 없습니다. index.html에 추가하는 것을 잊으셨나요? 또는 id 속성의 철자가 틀렸을 수 있습니다.",
"error.globalSync.connectFailed": "서버에 연결할 수 없습니다. `{{url}}`에서 서버가 실행 중인가요?", "error.serverSync.connectFailed": "서버에 연결할 수 없습니다. `{{url}}`에서 서버가 실행 중인가요?",
"directory.error.invalidUrl": "URL에 유효하지 않은 디렉터리가 있습니다.", "directory.error.invalidUrl": "URL에 유효하지 않은 디렉터리가 있습니다.",
"error.chain.unknown": "알 수 없는 오류", "error.chain.unknown": "알 수 없는 오류",
"error.chain.causedBy": "원인:", "error.chain.causedBy": "원인:",
@@ -839,8 +839,8 @@ export const dict = {
"common.key.insert": "Insert", "common.key.insert": "Insert",
"common.unknown": "알 수 없음", "common.unknown": "알 수 없음",
"error.page.circular": "[순환]", "error.page.circular": "[순환]",
"error.globalSDK.noServerAvailable": "사용 가능한 서버 없음", "error.serverSDK.noServerAvailable": "사용 가능한 서버 없음",
"error.globalSDK.serverNotAvailable": "서버를 사용할 수 없음", "error.serverSDK.serverNotAvailable": "서버를 사용할 수 없음",
"error.childStore.persistedCacheCreateFailed": "영구 캐시 생성 실패", "error.childStore.persistedCacheCreateFailed": "영구 캐시 생성 실패",
"error.childStore.persistedProjectMetadataCreateFailed": "영구 프로젝트 메타데이터 생성 실패", "error.childStore.persistedProjectMetadataCreateFailed": "영구 프로젝트 메타데이터 생성 실패",
"error.childStore.persistedProjectIconCreateFailed": "영구 프로젝트 아이콘 생성 실패", "error.childStore.persistedProjectIconCreateFailed": "영구 프로젝트 아이콘 생성 실패",
+3 -3
View File
@@ -463,7 +463,7 @@ export const dict = {
"error.dev.rootNotFound": "error.dev.rootNotFound":
"Rotelement ikke funnet. Glemte du å legge det til i index.html? Eller kanskje id-attributten er feilstavet?", "Rotelement ikke funnet. Glemte du å legge det til i index.html? Eller kanskje id-attributten er feilstavet?",
"error.globalSync.connectFailed": "Kunne ikke koble til server. Kjører det en server på `{{url}}`?", "error.serverSync.connectFailed": "Kunne ikke koble til server. Kjører det en server på `{{url}}`?",
"directory.error.invalidUrl": "Invalid directory in URL.", "directory.error.invalidUrl": "Invalid directory in URL.",
"error.chain.unknown": "Ukjent feil", "error.chain.unknown": "Ukjent feil",
@@ -927,8 +927,8 @@ export const dict = {
"common.key.insert": "Insert", "common.key.insert": "Insert",
"common.unknown": "ukjent", "common.unknown": "ukjent",
"error.page.circular": "[Sirkulær]", "error.page.circular": "[Sirkulær]",
"error.globalSDK.noServerAvailable": "Ingen server tilgjengelig", "error.serverSDK.noServerAvailable": "Ingen server tilgjengelig",
"error.globalSDK.serverNotAvailable": "Server ikke tilgjengelig", "error.serverSDK.serverNotAvailable": "Server ikke tilgjengelig",
"error.childStore.persistedCacheCreateFailed": "Kunne ikke opprette vedvarende hurtigbuffer", "error.childStore.persistedCacheCreateFailed": "Kunne ikke opprette vedvarende hurtigbuffer",
"error.childStore.persistedProjectMetadataCreateFailed": "Kunne ikke opprette vedvarende prosjektmetadata", "error.childStore.persistedProjectMetadataCreateFailed": "Kunne ikke opprette vedvarende prosjektmetadata",
"error.childStore.persistedProjectIconCreateFailed": "Kunne ikke opprette vedvarende prosjektikon", "error.childStore.persistedProjectIconCreateFailed": "Kunne ikke opprette vedvarende prosjektikon",
+3 -3
View File
@@ -414,7 +414,7 @@ export const dict = {
"error.page.version": "Wersja: {{version}}", "error.page.version": "Wersja: {{version}}",
"error.dev.rootNotFound": "error.dev.rootNotFound":
"Nie znaleziono elementu głównego. Czy zapomniałeś dodać go do swojego index.html? A może atrybut id został błędnie wpisany?", "Nie znaleziono elementu głównego. Czy zapomniałeś dodać go do swojego index.html? A może atrybut id został błędnie wpisany?",
"error.globalSync.connectFailed": "Nie można połączyć się z serwerem. Czy serwer działa pod adresem `{{url}}`?", "error.serverSync.connectFailed": "Nie można połączyć się z serwerem. Czy serwer działa pod adresem `{{url}}`?",
"directory.error.invalidUrl": "Nieprawidłowy katalog w URL.", "directory.error.invalidUrl": "Nieprawidłowy katalog w URL.",
"error.chain.unknown": "Nieznany błąd", "error.chain.unknown": "Nieznany błąd",
"error.chain.causedBy": "Spowodowany przez:", "error.chain.causedBy": "Spowodowany przez:",
@@ -848,8 +848,8 @@ export const dict = {
"common.key.insert": "Insert", "common.key.insert": "Insert",
"common.unknown": "nieznany", "common.unknown": "nieznany",
"error.page.circular": "[Cykliczne]", "error.page.circular": "[Cykliczne]",
"error.globalSDK.noServerAvailable": "Brak dostępnego serwera", "error.serverSDK.noServerAvailable": "Brak dostępnego serwera",
"error.globalSDK.serverNotAvailable": "Serwer niedostępny", "error.serverSDK.serverNotAvailable": "Serwer niedostępny",
"error.childStore.persistedCacheCreateFailed": "Nie udało się utworzyć trwałej pamięci podręcznej", "error.childStore.persistedCacheCreateFailed": "Nie udało się utworzyć trwałej pamięci podręcznej",
"error.childStore.persistedProjectMetadataCreateFailed": "Nie udało się utworzyć trwałych metadanych projektu", "error.childStore.persistedProjectMetadataCreateFailed": "Nie udało się utworzyć trwałych metadanych projektu",
"error.childStore.persistedProjectIconCreateFailed": "Nie udało się utworzyć trwałej ikony projektu", "error.childStore.persistedProjectIconCreateFailed": "Nie udało się utworzyć trwałej ikony projektu",
+3 -3
View File
@@ -461,7 +461,7 @@ export const dict = {
"error.dev.rootNotFound": "error.dev.rootNotFound":
"Корневой элемент не найден. Вы забыли добавить его в index.html? Или, может быть, атрибут id был написан неправильно?", "Корневой элемент не найден. Вы забыли добавить его в index.html? Или, может быть, атрибут id был написан неправильно?",
"error.globalSync.connectFailed": "Не удалось подключиться к серверу. Запущен ли сервер по адресу `{{url}}`?", "error.serverSync.connectFailed": "Не удалось подключиться к серверу. Запущен ли сервер по адресу `{{url}}`?",
"directory.error.invalidUrl": "Недопустимая директория в URL.", "directory.error.invalidUrl": "Недопустимая директория в URL.",
"error.chain.unknown": "Неизвестная ошибка", "error.chain.unknown": "Неизвестная ошибка",
@@ -929,8 +929,8 @@ export const dict = {
"common.key.insert": "Insert", "common.key.insert": "Insert",
"common.unknown": "неизвестно", "common.unknown": "неизвестно",
"error.page.circular": "[Циклично]", "error.page.circular": "[Циклично]",
"error.globalSDK.noServerAvailable": "Нет доступного сервера", "error.serverSDK.noServerAvailable": "Нет доступного сервера",
"error.globalSDK.serverNotAvailable": "Сервер недоступен", "error.serverSDK.serverNotAvailable": "Сервер недоступен",
"error.childStore.persistedCacheCreateFailed": "Не удалось создать постоянный кэш", "error.childStore.persistedCacheCreateFailed": "Не удалось создать постоянный кэш",
"error.childStore.persistedProjectMetadataCreateFailed": "Не удалось создать постоянные метаданные проекта", "error.childStore.persistedProjectMetadataCreateFailed": "Не удалось создать постоянные метаданные проекта",
"error.childStore.persistedProjectIconCreateFailed": "Не удалось создать постоянный значок проекта", "error.childStore.persistedProjectIconCreateFailed": "Не удалось создать постоянный значок проекта",
+3 -3
View File
@@ -459,7 +459,7 @@ export const dict = {
"error.dev.rootNotFound": "ไม่พบองค์ประกอบรูท คุณลืมเพิ่มใน index.html หรือบางทีแอตทริบิวต์ id อาจสะกดผิด?", "error.dev.rootNotFound": "ไม่พบองค์ประกอบรูท คุณลืมเพิ่มใน index.html หรือบางทีแอตทริบิวต์ id อาจสะกดผิด?",
"error.globalSync.connectFailed": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ มีเซิร์ฟเวอร์ทำงานอยู่ที่ `{{url}}` หรือไม่?", "error.serverSync.connectFailed": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ มีเซิร์ฟเวอร์ทำงานอยู่ที่ `{{url}}` หรือไม่?",
"directory.error.invalidUrl": "ไดเรกทอรีใน URL ไม่ถูกต้อง", "directory.error.invalidUrl": "ไดเรกทอรีใน URL ไม่ถูกต้อง",
"error.chain.unknown": "ข้อผิดพลาดที่ไม่รู้จัก", "error.chain.unknown": "ข้อผิดพลาดที่ไม่รู้จัก",
@@ -916,8 +916,8 @@ export const dict = {
"common.key.insert": "Insert", "common.key.insert": "Insert",
"common.unknown": "ไม่ทราบ", "common.unknown": "ไม่ทราบ",
"error.page.circular": "[วงกลม]", "error.page.circular": "[วงกลม]",
"error.globalSDK.noServerAvailable": "ไม่มีเซิร์ฟเวอร์", "error.serverSDK.noServerAvailable": "ไม่มีเซิร์ฟเวอร์",
"error.globalSDK.serverNotAvailable": "เซิร์ฟเวอร์ไม่พร้อมใช้งาน", "error.serverSDK.serverNotAvailable": "เซิร์ฟเวอร์ไม่พร้อมใช้งาน",
"error.childStore.persistedCacheCreateFailed": "ไม่สามารถสร้างแคชถาวร", "error.childStore.persistedCacheCreateFailed": "ไม่สามารถสร้างแคชถาวร",
"error.childStore.persistedProjectMetadataCreateFailed": "ไม่สามารถสร้างเมตาดาต้าโปรเจกต์ถาวร", "error.childStore.persistedProjectMetadataCreateFailed": "ไม่สามารถสร้างเมตาดาต้าโปรเจกต์ถาวร",
"error.childStore.persistedProjectIconCreateFailed": "ไม่สามารถสร้างไอคอนโปรเจกต์ถาวร", "error.childStore.persistedProjectIconCreateFailed": "ไม่สามารถสร้างไอคอนโปรเจกต์ถาวร",
+3 -3
View File
@@ -465,7 +465,7 @@ export const dict = {
"error.dev.rootNotFound": "error.dev.rootNotFound":
"Kök eleman bulunamadı. index.html dosyanıza eklemeyi unuttunuz mu? Ya da id özelliği yanlış mı yazıldı?", "Kök eleman bulunamadı. index.html dosyanıza eklemeyi unuttunuz mu? Ya da id özelliği yanlış mı yazıldı?",
"error.globalSync.connectFailed": "Sunucuya bağlanılamadı. `{{url}}` adresinde çalışan bir sunucu var mı?", "error.serverSync.connectFailed": "Sunucuya bağlanılamadı. `{{url}}` adresinde çalışan bir sunucu var mı?",
"directory.error.invalidUrl": "URL'de geçersiz dizin.", "directory.error.invalidUrl": "URL'de geçersiz dizin.",
"error.chain.unknown": "Bilinmeyen hata", "error.chain.unknown": "Bilinmeyen hata",
@@ -935,8 +935,8 @@ export const dict = {
"common.key.insert": "Insert", "common.key.insert": "Insert",
"common.unknown": "bilinmiyor", "common.unknown": "bilinmiyor",
"error.page.circular": "[Döngüsel]", "error.page.circular": "[Döngüsel]",
"error.globalSDK.noServerAvailable": "Sunucu yok", "error.serverSDK.noServerAvailable": "Sunucu yok",
"error.globalSDK.serverNotAvailable": "Sunucu mevcut değil", "error.serverSDK.serverNotAvailable": "Sunucu mevcut değil",
"error.childStore.persistedCacheCreateFailed": "Kalıcı önbellek oluşturulamadı", "error.childStore.persistedCacheCreateFailed": "Kalıcı önbellek oluşturulamadı",
"error.childStore.persistedProjectMetadataCreateFailed": "Kalıcı proje meta verileri oluşturulamadı", "error.childStore.persistedProjectMetadataCreateFailed": "Kalıcı proje meta verileri oluşturulamadı",
"error.childStore.persistedProjectIconCreateFailed": "Kalıcı proje simgesi oluşturulamadı", "error.childStore.persistedProjectIconCreateFailed": "Kalıcı proje simgesi oluşturulamadı",
+4 -5
View File
@@ -483,9 +483,9 @@ export const dict = {
"error.dev.rootNotFound": "error.dev.rootNotFound":
"Кореневий елемент не знайдено. Ви забули додати його до index.html? Або, можливо, атрибут id було написано з помилкою?", "Кореневий елемент не знайдено. Ви забули додати його до index.html? Або, можливо, атрибут id було написано з помилкою?",
"error.globalSync.connectFailed": "Не вдалося підключитися до сервера. Чи працює сервер за адресою `{{url}}`?", "error.serverSync.connectFailed": "Не вдалося підключитися до сервера. Чи працює сервер за адресою `{{url}}`?",
"error.globalSDK.noServerAvailable": "Сервер недоступний", "error.serverSDK.noServerAvailable": "Сервер недоступний",
"error.globalSDK.serverNotAvailable": "Сервер недоступний", "error.serverSDK.serverNotAvailable": "Сервер недоступний",
"error.childStore.persistedCacheCreateFailed": "Не вдалося створити постійний кеш", "error.childStore.persistedCacheCreateFailed": "Не вдалося створити постійний кеш",
"error.childStore.persistedProjectMetadataCreateFailed": "Не вдалося створити постійні метадані проєкту", "error.childStore.persistedProjectMetadataCreateFailed": "Не вдалося створити постійні метадані проєкту",
"error.childStore.persistedProjectIconCreateFailed": "Не вдалося створити постійну іконку проєкту", "error.childStore.persistedProjectIconCreateFailed": "Не вдалося створити постійну іконку проєкту",
@@ -759,8 +759,7 @@ export const dict = {
"settings.general.row.followup.option.queue": "Черга", "settings.general.row.followup.option.queue": "Черга",
"settings.general.row.followup.option.steer": "Керування", "settings.general.row.followup.option.steer": "Керування",
"settings.general.row.showFileTree.title": "Дерево файлів", "settings.general.row.showFileTree.title": "Дерево файлів",
"settings.general.row.showFileTree.description": "settings.general.row.showFileTree.description": "Показувати панель дерева файлів у сесіях на робочому столі",
"Показувати перемикач і панель дерева файлів у сесіях на робочому столі",
"settings.general.row.showNavigation.title": "Елементи навігації", "settings.general.row.showNavigation.title": "Елементи навігації",
"settings.general.row.showNavigation.description": "Показувати кнопки назад і вперед у заголовку робочого столу", "settings.general.row.showNavigation.description": "Показувати кнопки назад і вперед у заголовку робочого столу",
"settings.general.row.showSearch.title": "Палітра команд", "settings.general.row.showSearch.title": "Палітра команд",
+3 -3
View File
@@ -462,7 +462,7 @@ export const dict = {
"error.page.report.discord": "在 Discord 上", "error.page.report.discord": "在 Discord 上",
"error.page.version": "版本:{{version}}", "error.page.version": "版本:{{version}}",
"error.dev.rootNotFound": "未找到根元素。你是不是忘了把它添加到 index.html?或者 id 属性拼写错了?", "error.dev.rootNotFound": "未找到根元素。你是不是忘了把它添加到 index.html?或者 id 属性拼写错了?",
"error.globalSync.connectFailed": "无法连接到服务器。是否有服务器正在 `{{url}}` 运行?", "error.serverSync.connectFailed": "无法连接到服务器。是否有服务器正在 `{{url}}` 运行?",
"directory.error.invalidUrl": "URL 中的目录无效。", "directory.error.invalidUrl": "URL 中的目录无效。",
@@ -919,8 +919,8 @@ export const dict = {
"common.key.insert": "Insert", "common.key.insert": "Insert",
"common.unknown": "未知", "common.unknown": "未知",
"error.page.circular": "[循环]", "error.page.circular": "[循环]",
"error.globalSDK.noServerAvailable": "无可用服务器", "error.serverSDK.noServerAvailable": "无可用服务器",
"error.globalSDK.serverNotAvailable": "服务器不可用", "error.serverSDK.serverNotAvailable": "服务器不可用",
"error.childStore.persistedCacheCreateFailed": "创建持久化缓存失败", "error.childStore.persistedCacheCreateFailed": "创建持久化缓存失败",
"error.childStore.persistedProjectMetadataCreateFailed": "创建持久化项目元数据失败", "error.childStore.persistedProjectMetadataCreateFailed": "创建持久化项目元数据失败",
"error.childStore.persistedProjectIconCreateFailed": "创建持久化项目图标失败", "error.childStore.persistedProjectIconCreateFailed": "创建持久化项目图标失败",
+3 -3
View File
@@ -457,7 +457,7 @@ export const dict = {
"error.dev.rootNotFound": "找不到根元素。你是不是忘了把它新增到 index.html? 或者 id 屬性拼錯了?", "error.dev.rootNotFound": "找不到根元素。你是不是忘了把它新增到 index.html? 或者 id 屬性拼錯了?",
"error.globalSync.connectFailed": "無法連線到伺服器。是否有伺服器正在 `{{url}}` 執行?", "error.serverSync.connectFailed": "無法連線到伺服器。是否有伺服器正在 `{{url}}` 執行?",
"directory.error.invalidUrl": "URL 中的目錄無效。", "directory.error.invalidUrl": "URL 中的目錄無效。",
"error.chain.unknown": "未知錯誤", "error.chain.unknown": "未知錯誤",
@@ -907,8 +907,8 @@ export const dict = {
"common.key.insert": "Insert", "common.key.insert": "Insert",
"common.unknown": "未知", "common.unknown": "未知",
"error.page.circular": "[循環]", "error.page.circular": "[循環]",
"error.globalSDK.noServerAvailable": "無可用的伺服器", "error.serverSDK.noServerAvailable": "無可用的伺服器",
"error.globalSDK.serverNotAvailable": "伺服器無法使用", "error.serverSDK.serverNotAvailable": "伺服器無法使用",
"error.childStore.persistedCacheCreateFailed": "建立持續性快取失敗", "error.childStore.persistedCacheCreateFailed": "建立持續性快取失敗",
"error.childStore.persistedProjectMetadataCreateFailed": "建立持續性專案中繼資料失敗", "error.childStore.persistedProjectMetadataCreateFailed": "建立持續性專案中繼資料失敗",
"error.childStore.persistedProjectIconCreateFailed": "建立持續性專案圖示失敗", "error.childStore.persistedProjectIconCreateFailed": "建立持續性專案圖示失敗",
+7
View File
@@ -8,6 +8,13 @@
font-style: normal; font-style: normal;
} }
@font-face {
font-family: "Inter";
src: url("/assets/Inter.ttf") format("truetype");
font-weight: normal;
font-style: normal;
}
@layer components { @layer components {
@keyframes session-progress-whip { @keyframes session-progress-whip {
0% { 0% {
+3 -5
View File
@@ -6,7 +6,7 @@ import { createEffect, createMemo, createResource, type ParentProps, Show } from
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { LocalProvider } from "@/context/local" import { LocalProvider } from "@/context/local"
import { SDKProvider } from "@/context/sdk" import { SDKProvider } from "@/context/sdk"
import { SyncProvider, useSync } from "@/context/sync" import { useSync } from "@/context/sync"
import { decode64 } from "@/utils/base64" import { decode64 } from "@/utils/base64"
import { Schema } from "effect" import { Schema } from "effect"
@@ -81,10 +81,8 @@ export default function Layout(props: ParentProps) {
return ( return (
<Show when={resolved()} keyed> <Show when={resolved()} keyed>
{(resolved) => ( {(resolved) => (
<SDKProvider directory={() => resolved}> <SDKProvider directory={resolved}>
<SyncProvider> <DirectoryDataProvider directory={resolved}>{props.children}</DirectoryDataProvider>
<DirectoryDataProvider directory={resolved}>{props.children}</DirectoryDataProvider>
</SyncProvider>
</SDKProvider> </SDKProvider>
)} )}
</Show> </Show>
+417 -273
View File
@@ -9,6 +9,7 @@ import { Avatar as AvatarV2 } from "@opencode-ai/ui/v2/components/avatar-v2.jsx"
import { ButtonV2 } from "@opencode-ai/ui/v2/components/button-v2.jsx" import { ButtonV2 } from "@opencode-ai/ui/v2/components/button-v2.jsx"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/components/icon.jsx" import { Icon as IconV2 } from "@opencode-ai/ui/v2/components/icon.jsx"
import { IconButtonV2 } from "@opencode-ai/ui/v2/components/icon-button-v2.jsx" import { IconButtonV2 } from "@opencode-ai/ui/v2/components/icon-button-v2.jsx"
import { MenuV2 } from "@opencode-ai/ui/v2/components/menu-v2.jsx"
import { getAvatarColors, useLayout, type LocalProject } from "@/context/layout" import { getAvatarColors, useLayout, type LocalProject } from "@/context/layout"
import { useNavigate } from "@solidjs/router" import { useNavigate } from "@solidjs/router"
import { base64Encode } from "@opencode-ai/core/util/encode" import { base64Encode } from "@opencode-ai/core/util/encode"
@@ -18,24 +19,24 @@ import { DateTime } from "luxon"
import { useDialog } from "@opencode-ai/ui/context/dialog" import { useDialog } from "@opencode-ai/ui/context/dialog"
import { DialogSelectDirectory } from "@/components/dialog-select-directory" import { DialogSelectDirectory } from "@/components/dialog-select-directory"
import { DialogSelectServer } from "@/components/dialog-select-server" import { DialogSelectServer } from "@/components/dialog-select-server"
import { DialogSelectModel } from "@/components/dialog-select-model" import { ServerConnection, useServer } from "@/context/server"
import { useServer } from "@/context/server" import { useServerSync } from "@/context/server-sync"
import { useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useNotification } from "@/context/notification" import { useNotification } from "@/context/notification"
import { usePermission } from "@/context/permission" import { usePermission } from "@/context/permission"
import { displayName, getProjectAvatarSource, projectForSession, sortedRootSessions } from "@/pages/layout/helpers" import { displayName, getProjectAvatarSource, projectForSession, sortedRootSessions } from "@/pages/layout/helpers"
import { getFilename } from "@opencode-ai/core/util/path"
import { sessionTitle } from "@/utils/session-title" import { sessionTitle } from "@/utils/session-title"
import { pathKey } from "@/utils/path-key" import { pathKey } from "@/utils/path-key"
import { messageAgentColor } from "@/utils/agent" import { messageAgentColor } from "@/utils/agent"
import { sessionPermissionRequest } from "@/pages/session/composer/session-request-tree" import { sessionPermissionRequest } from "@/pages/session/composer/session-request-tree"
import { ServerHealthIndicator } from "@/components/server/server-row"
import { useServers } from "@/context/servers"
import { useSettings } from "@/context/settings"
const USE_HOME_DESIGN = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
const HOME_SESSION_LIMIT = 15 const HOME_SESSION_LIMIT = 15
const HOME_ROW = const HOME_ROW =
"flex min-w-0 w-full shrink-0 cursor-default items-center rounded-[6px] border-0 bg-transparent text-left [font-weight:530] text-v2-text-text-muted transition-colors duration-[120ms] ease-in-out hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none" "flex min-w-0 w-full shrink-0 cursor-default items-center rounded-[6px] border-0 bg-transparent text-left text-v2-text-text-muted transition-colors duration-[120ms] ease-in-out hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
const HOME_PROJECT_NAV_ROW = `${HOME_ROW} h-8 gap-1.5 px-3 [&>span]:min-w-0 [&>span]:overflow-hidden [&>span]:text-ellipsis [&>span]:whitespace-nowrap` const HOME_PROJECT_NAV_ROW = `${HOME_ROW} h-7 gap-2 px-1.5 [&>span]:min-w-0 [&>span]:overflow-hidden [&>span]:text-ellipsis [&>span]:whitespace-nowrap`
const HOME_SECTION_LABEL = "text-v2-text-text-muted [font-weight:440]" const HOME_SECTION_LABEL = "text-v2-text-text-muted [font-weight:440]"
type HomeSessionRecord = { type HomeSessionRecord = {
@@ -51,28 +52,32 @@ type HomeSessionGroup = {
} }
export default function Home() { export default function Home() {
if (USE_HOME_DESIGN) return <HomeDesign /> const settings = useSettings()
return <LegacyHome /> return (
<Show when={settings.general.newLayoutDesigns()} fallback={<LegacyHome />}>
<HomeDesign />
</Show>
)
} }
function HomeDesign() { function HomeDesign() {
const sync = useGlobalSync() const sync = useServerSync()
const layout = useLayout() const layout = useLayout()
const platform = usePlatform() const platform = usePlatform()
const dialog = useDialog() const dialog = useDialog()
const navigate = useNavigate() const navigate = useNavigate()
const server = useServer() const server = useServer()
const language = useLanguage() const language = useLanguage()
const notification = useNotification()
const [state, setState] = createStore({ search: "", project: undefined as string | undefined }) const [state, setState] = createStore({ search: "", project: undefined as string | undefined })
const projects = createMemo(() => layout.projects.list()) const projects = createMemo(() => layout.projects.list())
const selectedProject = createMemo( const selectedProject = createMemo(() => projects().find((project) => project.worktree === state.project))
() => projects().find((project) => project.worktree === state.project) ?? projects()[0], const directories = (project: LocalProject) => [project.worktree, ...(project.sandboxes ?? [])]
)
const projectDirectories = createMemo(() => { const projectDirectories = createMemo(() => {
const project = selectedProject() const project = selectedProject()
if (!project) return [] if (!project) return [...projects().flatMap((project) => directories(project))]
return [project.worktree, ...(project.sandboxes ?? [])] return directories(project)
}) })
const search = createMemo(() => state.search.trim()) const search = createMemo(() => state.search.trim())
const sessionLoad = useQuery(() => ({ const sessionLoad = useQuery(() => ({
@@ -86,8 +91,8 @@ function HomeDesign() {
const projectByID = createMemo( const projectByID = createMemo(
() => new Map(projects().flatMap((project) => (project.id ? [[project.id, project] as const] : []))), () => new Map(projects().flatMap((project) => (project.id ? [[project.id, project] as const] : []))),
) )
const records = createMemo(() => const records = createMemo(() => {
[ return [
...new Map( ...new Map(
projectDirectories() projectDirectories()
.flatMap((directory) => sortedRootSessions(sync.child(directory, { bootstrap: false })[0], Date.now())) .flatMap((directory) => sortedRootSessions(sync.child(directory, { bootstrap: false })[0], Date.now()))
@@ -109,8 +114,8 @@ function HomeDesign() {
if (!value) return true if (!value) return true
return `${record.session.title} ${record.projectName}`.toLowerCase().includes(value) return `${record.session.title} ${record.projectName}`.toLowerCase().includes(value)
}) })
.slice(0, HOME_SESSION_LIMIT), .slice(0, HOME_SESSION_LIMIT)
) })
const groups = createMemo(() => groupSessions(records(), language)) const groups = createMemo(() => groupSessions(records(), language))
function selectProject(directory: string) { function selectProject(directory: string) {
@@ -135,6 +140,26 @@ function HomeDesign() {
navigate(`/${base64Encode(project.worktree)}/session`) navigate(`/${base64Encode(project.worktree)}/session`)
} }
function openProjectNewSession(directory: string) {
layout.projects.open(directory)
server.projects.touch(directory)
navigate(`/${base64Encode(directory)}/session`)
}
const showEditProjectDialog = (project: LocalProject) => {
void import("@/components/dialog-edit-project").then((x) => {
dialog.show(() => <x.DialogEditProject project={project} />)
})
}
const unseenCount = (project: LocalProject) =>
directories(project).reduce((total, directory) => total + notification.project.unseenCount(directory), 0)
const clearNotifications = (project: LocalProject) =>
directories(project)
.filter((directory) => notification.project.unseenCount(directory) > 0)
.forEach((directory) => notification.project.markViewed(directory))
function openSession(session: Session) { function openSession(session: Session) {
const project = projectForSession(session, projects(), projectByID()) const project = projectForSession(session, projects(), projectByID())
layout.projects.open(project?.worktree ?? session.directory) layout.projects.open(project?.worktree ?? session.directory)
@@ -176,10 +201,17 @@ function HomeDesign() {
return ( return (
<div class="mx-auto grid w-full h-full max-w-[1080px] gap-8 px-6 pb-16 lg:grid-cols-[280px_minmax(0,720px)]"> <div class="mx-auto grid w-full h-full max-w-[1080px] gap-8 px-6 pb-16 lg:grid-cols-[280px_minmax(0,720px)]">
<HomeProjectColumn <HomeProjectColumn
projects={projects()} selectedProject={state.project}
selected={selectedProject()?.worktree}
selectProject={selectProject} selectProject={selectProject}
openNewSession={openProjectNewSession}
chooseProject={() => void chooseProject()} chooseProject={() => void chooseProject()}
editProject={showEditProjectDialog}
closeProject={(directory) => {
layout.projects.close(directory)
if (state.project === directory) setState("project", undefined)
}}
clearNotifications={clearNotifications}
unseenCount={unseenCount}
openSettings={openSettings} openSettings={openSettings}
openHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")} openHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
language={language} language={language}
@@ -189,58 +221,87 @@ function HomeDesign() {
class="min-w-0 flex-1 flex flex-col overflow-y-hidden pt-12" class="min-w-0 flex-1 flex flex-col overflow-y-hidden pt-12"
aria-label={language.t("sidebar.project.recentSessions")} aria-label={language.t("sidebar.project.recentSessions")}
> >
<HomeSessionSearch <Show
value={state.search} when={projectDirectories().length > 0}
placeholder={language.t("home.sessions.search.placeholder")} fallback={
onInput={(value) => setState("search", value)} <HomeEmptyState
/> icon="folder-add-left"
<div class="mt-3 overflow-auto flex-1"> title={language.t("home.empty.title")}
<div class="pt-3 flex flex-col gap-6"> description={language.t("home.empty.description")}
<Show when={!sessionLoad.isLoading} fallback={<HomeSessionSkeleton label={language.t("common.loading")} />}> action={language.t("home.project.add")}
onAction={() => void chooseProject()}
/>
}
>
<HomeSessionSearch
value={state.search}
placeholder={language.t("home.sessions.search.placeholder")}
onInput={(value) => setState("search", value)}
clearLabel={language.t("common.clear")}
onClear={() => setState("search", "")}
/>
<div class="mt-3 overflow-auto flex-1">
<div class="pt-3 flex flex-col gap-6">
<Show <Show
when={groups().length > 0} when={!sessionLoad.isLoading}
fallback={ fallback={<HomeSessionSkeleton label={language.t("common.loading")} />}
<div class="flex min-w-0 flex-col gap-4">
<HomeSessionGroupHeader title={language.t("home.sessions.empty")} onNewSession={openNewSession} />
</div>
}
> >
<For each={groups()}> <Show
{(group, index) => ( when={groups().length > 0}
<div class="flex min-w-0 flex-col gap-4"> fallback={
<HomeSessionGroupHeader <HomeEmptyState
title={group.title} icon="edit"
onNewSession={index() === 0 ? openNewSession : undefined} title={language.t("home.sessions.empty")}
/> description={language.t("home.sessions.empty.description")}
<div class="flex min-w-0 flex-col gap-px"> action={language.t("command.session.new")}
<For each={group.sessions}> onAction={openNewSession}
{(record) => <HomeSessionRow record={record} openSession={openSession} />} />
</For> }
>
<For each={groups()}>
{(group, index) => (
<div class="flex min-w-0 flex-col gap-4">
<HomeSessionGroupHeader
title={group.title}
onNewSession={index() === 0 ? openNewSession : undefined}
/>
<div class="flex min-w-0 flex-col gap-px">
<For each={group.sessions}>
{(record) => <HomeSessionRow record={record} openSession={openSession} />}
</For>
</div>
</div> </div>
</div> )}
)} </For>
</For> </Show>
</Show> </Show>
</Show> </div>
</div> </div>
</div> </Show>
</section> </section>
</div> </div>
) )
} }
function HomeProjectColumn(props: { function HomeProjectColumn(props: {
projects: LocalProject[] selectedProject?: string
selected?: string
selectProject: (directory: string) => void selectProject: (directory: string) => void
openNewSession: (directory: string) => void
chooseProject: () => void chooseProject: () => void
editProject: (project: LocalProject) => void
closeProject: (directory: string) => void
clearNotifications: (project: LocalProject) => void
unseenCount: (project: LocalProject) => number
openSettings: () => void openSettings: () => void
openHelp: () => void openHelp: () => void
language: ReturnType<typeof useLanguage> language: ReturnType<typeof useLanguage>
}) { }) {
const servers = useServers()
const layout = useLayout()
const projects = createMemo(() => layout.projects.list())
return ( return (
<aside class="flex min-w-0 flex-col lg:pt-[52px]" aria-label={props.language.t("home.projects")}> <aside class="flex min-w-0 flex-col lg:pt-[52px] gap-4" aria-label={props.language.t("home.projects")}>
<div class="flex h-7 min-w-0 items-center justify-between pl-3"> <div class="flex h-7 min-w-0 items-center justify-between pl-1.5">
<div class={HOME_SECTION_LABEL}>{props.language.t("home.projects")}</div> <div class={HOME_SECTION_LABEL}>{props.language.t("home.projects")}</div>
<IconButtonV2 <IconButtonV2
data-action="home-add-project" data-action="home-add-project"
@@ -252,39 +313,80 @@ function HomeProjectColumn(props: {
aria-label={props.language.t("home.project.add")} aria-label={props.language.t("home.project.add")}
/> />
</div> </div>
<div class="mt-4 flex max-h-[min(572px,calc(100vh_-_300px))] min-w-0 flex-col gap-1 overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"> <Show
<Show when={servers.list().length > 1}
when={props.projects.length > 0} fallback={
fallback={ <ProjectList
<button projects={projects()}
type="button" selectedProject={props.selectedProject}
class={`${HOME_PROJECT_NAV_ROW} text-v2-text-text-faint [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted`} onSelectedProjectChange={props.selectProject}
onClick={props.chooseProject} onChooseProject={props.chooseProject}
> openNewSession={props.openNewSession}
<IconV2 name="folder-add-left" size="small" /> editProject={props.editProject}
<span>{props.language.t("home.project.add")}</span> closeProject={props.closeProject}
</button> clearNotifications={props.clearNotifications}
} unseenCount={props.unseenCount}
> language={props.language}
<For each={props.projects}> />
{(project) => ( }
<button >
type="button" <For each={servers.list()}>
data-component="home-project-row" {(server) => {
class={HOME_PROJECT_NAV_ROW} const key = ServerConnection.key(server)
classList={{ "bg-v2-overlay-simple-overlay-hover": props.selected === project.worktree }} const healthy = () => !!servers.health[key]?.healthy
data-selected={props.selected === project.worktree ? "" : undefined} const [open, setOpen] = createSignal(true)
aria-current={props.selected === project.worktree ? "page" : undefined}
onClick={() => props.selectProject(project.worktree)} return (
> <div class="max-h-[min(572px,calc(100vh_-_300px))] min-w-0 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
<HomeProjectAvatar project={project} /> <div class="relative h-7 group">
<span>{displayName(project)}</span> <button
</button> class="w-full h-full px-1.5 gap-2 flex flex-row items-center hover:not-disabled:bg-v2-overlay-simple-overlay-hover rounded-[4px]"
)} disabled={!healthy()}
</For> onClick={() => setOpen((o) => !o)}
</Show> >
</div> <div class="size-4 flex items-center justify-center">
<div class="mt-4 flex min-w-0 flex-col gap-1"> <ServerHealthIndicator health={servers.health[key]} />
</div>
<div class="flex flex-row items-center gap-1">
<span>{server.displayName ?? new URL(server.http.url).host}</span>
<Show when={healthy()}>
<IconV2
name="outline-chevron-down"
class="text-v2-icon-icon-muted data-[open=false]:-rotate-90"
data-open={open()}
/>
</Show>
</div>
</button>
<IconButtonV2
class="absolute right-1 inset-y-1 opacity-0 group-hover:opacity-100"
name="out"
variant="ghost-muted"
size="small"
icon={<IconV2 name="outline-dots" class="text-v2-icon-icon-muted" />}
/>
</div>
<Show when={healthy() && open()}>
<div class="h-px bg-v2-border-border-base mx-3 my-1" />
<ProjectList
projects={projects()}
selectedProject={props.selectedProject}
onSelectedProjectChange={props.selectProject}
onChooseProject={props.chooseProject}
openNewSession={props.openNewSession}
editProject={props.editProject}
closeProject={props.closeProject}
clearNotifications={props.clearNotifications}
unseenCount={props.unseenCount}
language={props.language}
/>
</Show>
</div>
)
}}
</For>
</Show>
<div class="flex min-w-0 flex-col gap-1">
<button <button
type="button" type="button"
class={`${HOME_PROJECT_NAV_ROW} text-v2-text-text-faint [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted`} class={`${HOME_PROJECT_NAV_ROW} text-v2-text-text-faint [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted`}
@@ -306,6 +408,81 @@ function HomeProjectColumn(props: {
) )
} }
function HomeProjectRow(props: {
project: LocalProject
selected: boolean
unseenCount: number
selectProject: (directory: string) => void
openNewSession: (directory: string) => void
editProject: (project: LocalProject) => void
closeProject: (directory: string) => void
clearNotifications: (project: LocalProject) => void
language: ReturnType<typeof useLanguage>
}) {
const name = createMemo(() => displayName(props.project))
const [menuOpen, setMenuOpen] = createSignal(false)
return (
<div class="group/project relative flex h-8 min-w-0 items-center rounded-[6px]">
<button
type="button"
data-component="home-project-row"
class={`${HOME_PROJECT_NAV_ROW} pr-16 peer`}
classList={{ "bg-v2-overlay-simple-overlay-hover": props.selected }}
data-selected={props.selected ? "" : undefined}
aria-current={props.selected ? "page" : undefined}
onClick={() => props.selectProject(props.project.worktree)}
>
<HomeProjectAvatar project={props.project} />
<span>{name()}</span>
</button>
<div
class="absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-0.5 opacity-0 transition-opacity group-hover/project:opacity-100 peer-focus-visible:opacity-100 focus-within:opacity-100 data-[menu=true]:opacity-100"
data-menu={menuOpen()}
>
<MenuV2 gutter={4} modal={false} placement="bottom-end" open={menuOpen()} onOpenChange={setMenuOpen}>
<MenuV2.Trigger
as={IconButtonV2}
data-action="home-project-menu"
variant="ghost-muted"
size="small"
icon={<IconV2 name="outline-dots" />}
aria-label={props.language.t("common.moreOptions")}
/>
<MenuV2.Portal>
<MenuV2.Content>
<MenuV2.Item onSelect={() => props.openNewSession(props.project.worktree)}>
{props.language.t("command.session.new")}
</MenuV2.Item>
<MenuV2.Item onSelect={() => props.editProject(props.project)}>
{props.language.t("common.edit")}
</MenuV2.Item>
<MenuV2.Item disabled={props.unseenCount === 0} onSelect={() => props.clearNotifications(props.project)}>
{props.language.t("sidebar.project.clearNotifications")}
</MenuV2.Item>
<MenuV2.Separator />
<MenuV2.Item onSelect={() => props.closeProject(props.project.worktree)}>
{props.language.t("common.close")}
</MenuV2.Item>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
<IconButtonV2
data-action="home-project-new-session"
variant="ghost-muted"
size="small"
icon={<IconV2 name="edit" />}
aria-label={props.language.t("command.session.new")}
onClick={(event) => {
event.stopPropagation()
props.openNewSession(props.project.worktree)
}}
/>
</div>
</div>
)
}
function HomeProjectAvatar(props: { project: LocalProject }) { function HomeProjectAvatar(props: { project: LocalProject }) {
const name = createMemo(() => displayName(props.project)) const name = createMemo(() => displayName(props.project))
return ( return (
@@ -320,7 +497,13 @@ function HomeProjectAvatar(props: { project: LocalProject }) {
) )
} }
function HomeSessionSearch(props: { value: string; placeholder: string; onInput: (value: string) => void }) { function HomeSessionSearch(props: {
value: string
placeholder: string
clearLabel: string
onInput: (value: string) => void
onClear: () => void
}) {
return ( return (
<label class="ml-4 flex h-9 w-[calc(100%_-_48px)] sticky top-0 inset-x-0 items-center gap-2 rounded-[6px] bg-v2-background-bg-deep px-3 py-1 text-v2-icon-icon-muted transition-[background-color,box-shadow] duration-[120ms] ease-in-out focus-within:bg-v2-background-bg-base focus-within:shadow-[0_0_0_0.5px_var(--v2-border-border-focus),var(--v2-elevation-raised)]"> <label class="ml-4 flex h-9 w-[calc(100%_-_48px)] sticky top-0 inset-x-0 items-center gap-2 rounded-[6px] bg-v2-background-bg-deep px-3 py-1 text-v2-icon-icon-muted transition-[background-color,box-shadow] duration-[120ms] ease-in-out focus-within:bg-v2-background-bg-base focus-within:shadow-[0_0_0_0.5px_var(--v2-border-border-focus),var(--v2-elevation-raised)]">
<IconV2 name="magnifying-glass" size="small" /> <IconV2 name="magnifying-glass" size="small" />
@@ -331,10 +514,46 @@ function HomeSessionSearch(props: { value: string; placeholder: string; onInput:
aria-label={props.placeholder} aria-label={props.placeholder}
onInput={(event) => props.onInput(event.currentTarget.value)} onInput={(event) => props.onInput(event.currentTarget.value)}
/> />
<Show when={props.value.trim()}>
<button
type="button"
class="flex size-5 shrink-0 items-center justify-center rounded text-v2-icon-icon-muted hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
aria-label={props.clearLabel}
onClick={(event) => {
event.preventDefault()
props.onClear()
}}
>
<Icon name="close-small" size="small" />
</button>
</Show>
</label> </label>
) )
} }
function HomeEmptyState(props: {
icon: Parameters<typeof IconV2>[0]["name"]
title: string
description: string
action: string
onAction: () => void
}) {
return (
<div class="flex min-h-[320px] flex-1 flex-col items-center justify-center gap-4 px-6 text-center">
<div class="flex size-10 items-center justify-center rounded-[10px] bg-v2-background-bg-deep text-v2-icon-icon-muted shadow-[var(--v2-elevation-raised)]">
<IconV2 name={props.icon} />
</div>
<div class="flex max-w-[320px] flex-col gap-1">
<div class="text-v2-text-text-base [font-weight:530]">{props.title}</div>
<div class="text-v2-text-text-muted [font-weight:440]">{props.description}</div>
</div>
<ButtonV2 variant="neutral" size="normal" icon={props.icon} onClick={props.onAction}>
{props.action}
</ButtonV2>
</div>
)
}
function HomeSessionGroupHeader(props: { title: string; onNewSession?: () => void }) { function HomeSessionGroupHeader(props: { title: string; onNewSession?: () => void }) {
const language = useLanguage() const language = useLanguage()
return ( return (
@@ -347,7 +566,7 @@ function HomeSessionGroupHeader(props: { title: string; onNewSession?: () => voi
variant="ghost" variant="ghost"
size="normal" size="normal"
icon="edit" icon="edit"
class="h-7 px-2 text-v2-text-text-muted [font-weight:530]" class="h-7 px-2 text-v2-text-text-muted"
onClick={onNewSession()} onClick={onNewSession()}
> >
{language.t("command.session.new")} {language.t("command.session.new")}
@@ -359,10 +578,10 @@ function HomeSessionGroupHeader(props: { title: string; onNewSession?: () => voi
} }
function HomeSessionRow(props: { record: HomeSessionRecord; openSession: (session: Session) => void }) { function HomeSessionRow(props: { record: HomeSessionRecord; openSession: (session: Session) => void }) {
const globalSync = useGlobalSync() const serverSync = useServerSync()
const notification = useNotification() const notification = useNotification()
const permission = usePermission() const permission = usePermission()
const [sessionStore] = globalSync.child(props.record.session.directory, { bootstrap: false }) const [sessionStore] = serverSync.child(props.record.session.directory, { bootstrap: false })
const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id) const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id)
const unseenCount = createMemo(() => notification.session.unseenCount(props.record.session.id)) const unseenCount = createMemo(() => notification.session.unseenCount(props.record.session.id))
const hasError = createMemo(() => notification.session.unseenHasError(props.record.session.id)) const hasError = createMemo(() => notification.session.unseenHasError(props.record.session.id))
@@ -460,18 +679,14 @@ function groupSessions(records: HomeSessionRecord[], language: ReturnType<typeof
} }
function LegacyHome() { function LegacyHome() {
const sync = useGlobalSync() const sync = useServerSync()
const layout = useLayout() const layout = useLayout()
const platform = usePlatform() const platform = usePlatform()
const dialog = useDialog() const dialog = useDialog()
const navigate = useNavigate() const navigate = useNavigate()
const servers = useServers()
const server = useServer() const server = useServer()
const language = useLanguage() const language = useLanguage()
const [promptText, setPromptText] = createSignal("")
const [selectedAgent, setSelectedAgent] = createSignal("frontend-specialist")
const [showProjectsDropdown, setShowProjectsDropdown] = createSignal(false)
const homedir = createMemo(() => sync.data.path.home) const homedir = createMemo(() => sync.data.path.home)
const recent = createMemo(() => { const recent = createMemo(() => {
return sync.data.project return sync.data.project
@@ -480,10 +695,8 @@ function LegacyHome() {
.slice(0, 5) .slice(0, 5)
}) })
const currentProject = createMemo(() => recent()[0]?.worktree)
const serverDotClass = createMemo(() => { const serverDotClass = createMemo(() => {
const healthy = server.healthy() const healthy = servers.health[server.key]?.healthy
if (healthy === true) return "bg-icon-success-base" if (healthy === true) return "bg-icon-success-base"
if (healthy === false) return "bg-icon-critical-base" if (healthy === false) return "bg-icon-critical-base"
return "bg-border-weak-base" return "bg-border-weak-base"
@@ -520,188 +733,119 @@ function LegacyHome() {
} }
} }
function handleModelSelect() {
dialog.show(() => <DialogSelectModel />)
}
function toggleAgent() {
const agents = ["frontend-specialist", "build", "general"]
const nextIndex = (agents.indexOf(selectedAgent()) + 1) % agents.length
setSelectedAgent(agents[nextIndex])
}
function handleSubmit() {
const projectToOpen = currentProject()
if (projectToOpen) {
openProject(projectToOpen)
} else {
chooseProject()
}
}
const activeModelName = createMemo(() => {
const model = sync.data.config.model
if (!model) return "GPT-5.7 Pro"
const parts = model.split("/")
return parts[parts.length - 1]
})
return ( return (
<div class="mx-auto mt-24 w-full max-w-2xl px-6 flex flex-col items-center"> <div class="mx-auto mt-55 w-full md:w-auto px-4">
<div class="flex flex-col items-center gap-3 mb-10"> <Logo class="md:w-xl opacity-12" />
<div onClick={chooseProject} class="cursor-pointer hover:opacity-25 transition-opacity duration-200"> <Button
<Logo class="w-48 opacity-15" /> size="large"
</div> variant="ghost"
<Button class="mt-4 mx-auto text-14-regular text-text-weak"
size="normal" onClick={() => dialog.show(() => <DialogSelectServer />)}
variant="ghost" >
class="text-12-regular text-text-weak px-3" <div
onClick={() => dialog.show(() => <DialogSelectServer />)} classList={{
> "size-2 rounded-full": true,
<div [serverDotClass()]: true,
classList={{ }}
"size-1.5 rounded-full mr-2": true, />
[serverDotClass()]: true, {server.name}
}} </Button>
/>
{server.name}
</Button>
</div>
<Switch> <Switch>
<Match when={recent().length > 0}> <Match when={sync.data.project.length > 0}>
<div class="w-full flex flex-col items-center gap-6"> <div class="mt-20 w-full flex flex-col gap-4">
<div class="text-20-medium text-text-strong text-center">{language.t("session.new.title")}</div> <div class="flex gap-2 items-center justify-between pl-3">
<div class="text-14-medium text-text-strong">{language.t("home.recentProjects")}</div>
<div class="w-full bg-surface-base border border-border-base rounded-xl p-4 flex flex-col gap-3 shadow-md relative"> <Button icon="folder-add-left" size="normal" class="pl-2 pr-3" onClick={chooseProject}>
<textarea {language.t("command.project.open")}
class="bg-transparent border-none outline-none text-14-regular text-text-base placeholder-text-weak w-full resize-none h-20 focus:outline-none" </Button>
placeholder="Ask anything, / for commands, @ for context..."
value={promptText()}
onInput={(e) => setPromptText(e.currentTarget.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault()
handleSubmit()
}
}}
/>
<div class="flex flex-wrap items-center gap-2 pt-3 border-t border-border-weak-base">
<Button
size="small"
variant="ghost"
class="text-12-medium text-text-weak hover:text-text-strong flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base hover:bg-surface-raised-base-hover border border-border-weak-base rounded-md"
onClick={toggleAgent}
>
<Icon name="sliders" size="small" class="shrink-0" />
<span>Agent: {selectedAgent()}</span>
</Button>
<Button
size="small"
variant="ghost"
class="text-12-medium text-text-weak hover:text-text-strong flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base hover:bg-surface-raised-base-hover border border-border-weak-base rounded-md"
onClick={handleModelSelect}
>
<Icon name="brain" size="small" class="shrink-0" />
<span>Model: {activeModelName()}</span>
</Button>
<div class="relative">
<Button
size="small"
variant="ghost"
class="text-12-medium text-text-weak hover:text-text-strong flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base hover:bg-surface-raised-base-hover border border-border-weak-base rounded-md"
onClick={() => setShowProjectsDropdown(!showProjectsDropdown())}
>
<Icon name="folder" size="small" class="shrink-0" />
<span>Project: {currentProject() ? getFilename(currentProject()) : "Select Project"}</span>
</Button>
<Show when={showProjectsDropdown()}>
<div class="absolute left-0 mt-1 w-64 bg-surface-raised-base border border-border-base rounded-lg p-2 shadow-lg z-50 flex flex-col gap-1">
<div class="text-10-semibold text-text-weak px-2 py-1 uppercase tracking-wider">
{language.t("home.recentProjects")}
</div>
<For each={recent()}>
{(project) => (
<button
class="text-12-mono text-left px-2 py-1.5 hover:bg-surface-raised-base-hover rounded flex items-center justify-between w-full"
onClick={() => {
openProject(project.worktree)
setShowProjectsDropdown(false)
}}
>
<span class="truncate">{getFilename(project.worktree)}</span>
<span class="text-10-regular text-text-weak shrink-0 pl-2">
{DateTime.fromMillis(project.time.updated ?? project.time.created).toRelative()}
</span>
</button>
)}
</For>
<div class="border-t border-border-weak-base my-1" />
<button
class="text-12-medium text-text-strong text-left px-2 py-1.5 hover:bg-surface-raised-base-hover rounded flex items-center gap-2 w-full"
onClick={() => {
setShowProjectsDropdown(false)
chooseProject()
}}
>
<Icon name="folder-add-left" size="small" />
{language.t("command.project.open")}
</button>
</div>
</Show>
</div>
<Button
size="small"
variant="ghost"
class="text-12-medium text-text-weak flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base border border-border-weak-base rounded-md cursor-default pointer-events-none"
>
<Icon name="branch" size="small" class="shrink-0" />
<span>Branch: dev</span>
</Button>
</div>
</div> </div>
<ul class="flex flex-col gap-2">
<For each={recent()}>
{(project) => (
<Button
size="large"
variant="ghost"
class="text-14-mono text-left justify-between px-3"
onClick={() => openProject(project.worktree)}
>
{project.worktree.replace(homedir(), "~")}
<div class="text-14-regular text-text-weak">
{DateTime.fromMillis(project.time.updated ?? project.time.created).toRelative()}
</div>
</Button>
)}
</For>
</ul>
</div>
</Match>
<Match when={!sync.ready}>
<div class="mt-30 mx-auto flex flex-col items-center gap-3">
<div class="text-12-regular text-text-weak">{language.t("common.loading")}</div>
<Button class="px-3" onClick={chooseProject}>
{language.t("command.project.open")}
</Button>
</div> </div>
</Match> </Match>
<Match when={true}> <Match when={true}>
<div class="w-full flex flex-col items-center gap-6"> <div class="mt-30 mx-auto flex flex-col items-center gap-3">
<div class="text-20-medium text-text-strong text-center">{language.t("home.empty.title")}</div> <Icon name="folder-add-left" size="large" />
<div class="flex flex-col gap-1 items-center justify-center">
<div class="w-full bg-surface-base border border-border-base rounded-xl p-4 flex flex-col gap-3 shadow-md"> <div class="text-14-medium text-text-strong">{language.t("home.empty.title")}</div>
<div class="text-14-regular text-text-weak w-full min-h-[4rem] cursor-pointer" onClick={chooseProject}> <div class="text-12-regular text-text-weak">{language.t("home.empty.description")}</div>
Ask anything, / for commands, @ for context...
</div>
<div class="flex flex-wrap items-center gap-2 pt-3 border-t border-border-weak-base">
<Button
size="small"
variant="ghost"
class="text-12-medium text-text-weak hover:text-text-strong flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base hover:bg-surface-raised-base-hover border border-border-weak-base rounded-md"
onClick={chooseProject}
>
<Icon name="folder" size="small" class="shrink-0" />
<span>Open project</span>
</Button>
<Button
size="small"
variant="ghost"
class="text-12-medium text-text-weak hover:text-text-strong flex items-center gap-1.5 px-2.5 py-1 bg-surface-raised-base hover:bg-surface-raised-base-hover border border-border-weak-base rounded-md"
onClick={handleModelSelect}
>
<Icon name="brain" size="small" class="shrink-0" />
<span>Model: {activeModelName()}</span>
</Button>
</div>
</div> </div>
<Button class="px-3 mt-1" onClick={chooseProject}>
{language.t("command.project.open")}
</Button>
</div> </div>
</Match> </Match>
</Switch> </Switch>
</div> </div>
) )
} }
function ProjectList(props: {
projects: LocalProject[]
selectedProject?: string
onSelectedProjectChange?(project: string): void
onChooseProject?(): void
openNewSession: (directory: string) => void
editProject: (project: LocalProject) => void
closeProject: (directory: string) => void
clearNotifications: (project: LocalProject) => void
unseenCount: (project: LocalProject) => number
language: ReturnType<typeof useLanguage>
}) {
return (
<Show
when={props.projects.length > 0}
fallback={
<button
type="button"
class={`${HOME_PROJECT_NAV_ROW} text-v2-text-text-faint [&>[data-slot=icon-svg]]:text-v2-icon-icon-muted`}
onClick={() => props.onChooseProject?.()}
>
<IconV2 name="folder-add-left" size="small" />
<span>{props.language.t("home.project.add")}</span>
</button>
}
>
<div class="flex flex-col gap-1">
<For each={props.projects}>
{(project) => (
<HomeProjectRow
project={project}
selected={props.selectedProject === project.worktree}
unseenCount={props.unseenCount(project)}
selectProject={(directory) => props.onSelectedProjectChange?.(directory)}
openNewSession={props.openNewSession}
editProject={props.editProject}
closeProject={props.closeProject}
clearNotifications={props.clearNotifications}
language={props.language}
/>
)}
</For>
</div>
</Show>
)
}
+218 -216
View File
@@ -16,7 +16,7 @@ import { makeEventListener } from "@solid-primitives/event-listener"
import { useLocation, useNavigate, useParams } from "@solidjs/router" import { useLocation, useNavigate, useParams } from "@solidjs/router"
import { useQuery } from "@tanstack/solid-query" import { useQuery } from "@tanstack/solid-query"
import { useLayout, LocalProject } from "@/context/layout" import { useLayout, LocalProject } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync" import { useServerSync } from "@/context/server-sync"
import { Persist, persisted } from "@/utils/persist" import { Persist, persisted } from "@/utils/persist"
import { base64Encode } from "@opencode-ai/core/util/encode" import { base64Encode } from "@opencode-ai/core/util/encode"
import { decode64 } from "@/utils/base64" import { decode64 } from "@/utils/base64"
@@ -35,7 +35,7 @@ import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, close
import type { DragEvent } from "@thisbeyond/solid-dnd" import type { DragEvent } from "@thisbeyond/solid-dnd"
import { useProviders } from "@/hooks/use-providers" import { useProviders } from "@/hooks/use-providers"
import { showToast, Toast, toaster } from "@opencode-ai/ui/toast" import { showToast, Toast, toaster } from "@opencode-ai/ui/toast"
import { useGlobalSDK } from "@/context/global-sdk" import { useServerSDK } from "@/context/server-sdk"
import { clearWorkspaceTerminals, getTerminalServerScope } from "@/context/terminal" import { clearWorkspaceTerminals, getTerminalServerScope } from "@/context/terminal"
import { dropSessionCaches, pickSessionCacheEvictions } from "@/context/global-sync/session-cache" import { dropSessionCaches, pickSessionCacheEvictions } from "@/context/global-sync/session-cache"
import { import {
@@ -89,8 +89,6 @@ import {
import { ProjectDragOverlay, SortableProject, type ProjectSidebarContext } from "./layout/sidebar-project" import { ProjectDragOverlay, SortableProject, type ProjectSidebarContext } from "./layout/sidebar-project"
import { SidebarContent } from "./layout/sidebar-shell" import { SidebarContent } from "./layout/sidebar-shell"
const USE_HOME_DESIGN = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
export default function Layout(props: ParentProps) { export default function Layout(props: ParentProps) {
const [store, setStore, , ready] = persisted( const [store, setStore, , ready] = persisted(
Persist.global("layout.page", ["layout.page.v1"]), Persist.global("layout.page", ["layout.page.v1"]),
@@ -113,8 +111,8 @@ export default function Layout(props: ParentProps) {
let dialogDead = false let dialogDead = false
const params = useParams() const params = useParams()
const globalSDK = useGlobalSDK() const serverSDK = useServerSDK()
const globalSync = useGlobalSync() const serverSync = useServerSync()
const layout = useLayout() const layout = useLayout()
const layoutReady = createMemo(() => layout.ready()) const layoutReady = createMemo(() => layout.ready())
const platform = usePlatform() const platform = usePlatform()
@@ -129,6 +127,7 @@ export default function Layout(props: ParentProps) {
const command = useCommand() const command = useCommand()
const theme = useTheme() const theme = useTheme()
const language = useLanguage() const language = useLanguage()
const newDesign = createMemo(() => settings.general.newLayoutDesigns())
const initialDirectory = decode64(params.dir) const initialDirectory = decode64(params.dir)
const location = useLocation() const location = useLocation()
const route = createMemo(() => { const route = createMemo(() => {
@@ -136,7 +135,7 @@ export default function Layout(props: ParentProps) {
if (!slug) return { slug, dir: "" } if (!slug) return { slug, dir: "" }
const dir = decode64(slug) const dir = decode64(slug)
if (!dir) return { slug, dir: "" } if (!dir) return { slug, dir: "" }
const store = globalSync.peek(dir, { bootstrap: false }) const store = serverSync.peek(dir, { bootstrap: false })
return { return {
slug, slug,
store, store,
@@ -154,7 +153,7 @@ export default function Layout(props: ParentProps) {
const currentDir = createMemo(() => route().dir) const currentDir = createMemo(() => route().dir)
const [state, setState] = createStore({ const [state, setState] = createStore({
autoselect: !initialDirectory && !USE_HOME_DESIGN, autoselect: !initialDirectory && !newDesign(),
busyWorkspaces: {} as Record<string, boolean>, busyWorkspaces: {} as Record<string, boolean>,
hoverProject: undefined as string | undefined, hoverProject: undefined as string | undefined,
scrollSessionKey: undefined as string | undefined, scrollSessionKey: undefined as string | undefined,
@@ -226,7 +225,7 @@ export default function Layout(props: ParentProps) {
active: () => state.hoverProject, active: () => state.hoverProject,
el: () => state.nav?.querySelector<HTMLElement>("[data-component='sidebar-rail']") ?? state.nav, el: () => state.nav?.querySelector<HTMLElement>("[data-component='sidebar-rail']") ?? state.nav,
onActivate: (directory) => { onActivate: (directory) => {
globalSync.child(directory) serverSync.child(directory)
setState("hoverProject", directory) setState("hoverProject", directory)
}, },
}) })
@@ -410,7 +409,7 @@ export default function Layout(props: ParentProps) {
alertedAtBySession.delete(sessionKey) alertedAtBySession.delete(sessionKey)
} }
const unsub = globalSDK.event.listen((e) => { const unsub = serverSDK.event.listen((e) => {
if (e.details?.type === "worktree.ready") { if (e.details?.type === "worktree.ready") {
setBusy(e.name, false) setBusy(e.name, false)
WorktreeState.ready(e.name) WorktreeState.ready(e.name)
@@ -444,7 +443,7 @@ export default function Layout(props: ParentProps) {
const props = e.details.properties const props = e.details.properties
if (e.details.type === "permission.asked" && permission.autoResponds(e.details.properties, directory)) return if (e.details.type === "permission.asked" && permission.autoResponds(e.details.properties, directory)) return
const [store] = globalSync.child(directory, { bootstrap: false }) const [store] = serverSync.child(directory, { bootstrap: false })
const session = store.session.find((s) => s.id === props.sessionID) const session = store.session.find((s) => s.id === props.sessionID)
const sessionKey = `${directory}:${props.sessionID}` const sessionKey = `${directory}:${props.sessionID}`
@@ -507,7 +506,7 @@ export default function Layout(props: ParentProps) {
if (!currentDir() || !currentSession) return if (!currentDir() || !currentSession) return
const sessionKey = `${currentDir()}:${currentSession}` const sessionKey = `${currentDir()}:${currentSession}`
dismissSessionAlert(sessionKey) dismissSessionAlert(sessionKey)
const [store] = globalSync.child(currentDir(), { bootstrap: false }) const [store] = serverSync.child(currentDir(), { bootstrap: false })
const childSessions = store.session.filter((s) => s.parentID === currentSession) const childSessions = store.session.filter((s) => s.parentID === currentSession)
for (const child of childSessions) { for (const child of childSessions) {
dismissSessionAlert(`${currentDir()}:${child.id}`) dismissSessionAlert(`${currentDir()}:${child.id}`)
@@ -545,11 +544,11 @@ export default function Layout(props: ParentProps) {
const direct = projects.find((p) => pathKey(p.worktree) === key) const direct = projects.find((p) => pathKey(p.worktree) === key)
if (direct) return direct if (direct) return direct
const [child] = globalSync.child(directory, { bootstrap: false }) const [child] = serverSync.child(directory, { bootstrap: false })
const id = child.project const id = child.project
if (!id) return if (!id) return
const meta = globalSync.data.project.find((p) => p.id === id) const meta = serverSync.data.project.find((p) => p.id === id)
const root = meta?.worktree const root = meta?.worktree
if (!root) return if (!root) return
@@ -640,7 +639,7 @@ export default function Layout(props: ParentProps) {
const result: Session[] = [] const result: Session[] = []
for (const dir of dirs) { for (const dir of dirs) {
const [dirStore] = globalSync.child(dir, { bootstrap: true }) const [dirStore] = serverSync.child(dir, { bootstrap: true })
const dirSessions = sortedRootSessions(dirStore, now) const dirSessions = sortedRootSessions(dirStore, now)
result.push(...dirSessions) result.push(...dirSessions)
} }
@@ -692,7 +691,7 @@ export default function Layout(props: ParentProps) {
createEffect(() => { createEffect(() => {
route() route()
globalSDK.url serverSDK.url
prefetchToken.value += 1 prefetchToken.value += 1
clearSessionPrefetchInflight() clearSessionPrefetchInflight()
@@ -739,13 +738,13 @@ export default function Layout(props: ParentProps) {
} }
async function prefetchMessages(directory: string, sessionID: string, token: number) { async function prefetchMessages(directory: string, sessionID: string, token: number) {
const [store, setStore] = globalSync.child(directory, { bootstrap: false }) const [store, setStore] = serverSync.child(directory, { bootstrap: false })
return runSessionPrefetch({ return runSessionPrefetch({
directory, directory,
sessionID, sessionID,
task: (rev) => task: (rev) =>
retry(() => globalSDK.client.session.messages({ directory, sessionID, limit: prefetchChunk })) retry(() => serverSDK.client.session.messages({ directory, sessionID, limit: prefetchChunk }))
.then((messages) => { .then((messages) => {
if (prefetchToken.value !== token) return if (prefetchToken.value !== token) return
if (!isSessionPrefetchCurrent(directory, sessionID, rev)) return if (!isSessionPrefetchCurrent(directory, sessionID, rev)) return
@@ -765,7 +764,7 @@ export default function Layout(props: ParentProps) {
if (stale.length > 0) { if (stale.length > 0) {
clearSessionPrefetch(directory, stale) clearSessionPrefetch(directory, stale)
for (const id of stale) { for (const id of stale) {
globalSync.todo.set(id, undefined) serverSync.todo.set(id, undefined)
} }
} }
@@ -830,7 +829,7 @@ export default function Layout(props: ParentProps) {
const directory = session.directory const directory = session.directory
if (!directory) return if (!directory) return
const [store] = globalSync.child(directory, { bootstrap: false }) const [store] = serverSync.child(directory, { bootstrap: false })
const cached = untrack(() => { const cached = untrack(() => {
const info = getSessionPrefetch(directory, session.id) const info = getSessionPrefetch(directory, session.id)
return shouldSkipSessionPrefetch({ return shouldSkipSessionPrefetch({
@@ -935,7 +934,7 @@ export default function Layout(props: ParentProps) {
if (!target) return if (!target) return
// warm up child store to prevent flicker // warm up child store to prevent flicker
globalSync.child(target.worktree) serverSync.child(target.worktree)
void openProject(target.worktree) void openProject(target.worktree)
} }
@@ -944,7 +943,7 @@ export default function Layout(props: ParentProps) {
const target = projects[index] const target = projects[index]
if (!target) return if (!target) return
globalSync.child(target.worktree) serverSync.child(target.worktree)
void openProject(target.worktree) void openProject(target.worktree)
} }
@@ -973,12 +972,12 @@ export default function Layout(props: ParentProps) {
} }
async function archiveSession(session: Session) { async function archiveSession(session: Session) {
const [store, setStore] = globalSync.child(session.directory) const [store, setStore] = serverSync.child(session.directory)
const sessions = store.session ?? [] const sessions = store.session ?? []
const index = sessions.findIndex((s) => s.id === session.id) const index = sessions.findIndex((s) => s.id === session.id)
const nextSession = sessions[index + 1] ?? sessions[index - 1] const nextSession = sessions[index + 1] ?? sessions[index - 1]
await globalSDK.client.session.update({ await serverSDK.client.session.update({
directory: session.directory, directory: session.directory,
sessionID: session.id, sessionID: session.id,
time: { archived: Date.now() }, time: { archived: Date.now() },
@@ -1028,19 +1027,6 @@ export default function Layout(props: ParentProps) {
keybind: "mod+alt+arrowdown", keybind: "mod+alt+arrowdown",
onSelect: () => navigateProjectByOffset(1), 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", id: "provider.connect",
title: language.t("command.provider.connect"), title: language.t("command.provider.connect"),
@@ -1155,6 +1141,21 @@ export default function Layout(props: ParentProps) {
}, },
] ]
if (!newDesign())
Array.from({ length: 9 }, (_, i) => {
const index = i
const number = index + 1
commands.push({
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),
})
})
for (const [id] of availableThemeEntries()) { for (const [id] of availableThemeEntries()) {
commands.push({ commands.push({
id: `theme.set.${id}`, id: `theme.set.${id}`,
@@ -1244,11 +1245,11 @@ export default function Layout(props: ParentProps) {
) )
if (known) return known[0] if (known) return known[0]
const [child] = globalSync.child(directory, { bootstrap: false }) const [child] = serverSync.child(directory, { bootstrap: false })
const id = child.project const id = child.project
if (!id) return directory if (!id) return directory
const meta = globalSync.data.project.find((item) => item.id === id) const meta = serverSync.data.project.find((item) => item.id === id)
return meta?.worktree ?? directory return meta?.worktree ?? directory
} }
@@ -1296,7 +1297,7 @@ export default function Layout(props: ParentProps) {
} }
const refreshDirs = async (target?: string) => { const refreshDirs = async (target?: string) => {
if (!target || target === root || canOpen(target)) return canOpen(target) if (!target || target === root || canOpen(target)) return canOpen(target)
const listed = await globalSDK.client.worktree const listed = await serverSDK.client.worktree
.list({ directory: root }) .list({ directory: root })
.then((x) => x.data ?? []) .then((x) => x.data ?? [])
.catch(() => [] as string[]) .catch(() => [] as string[])
@@ -1305,13 +1306,13 @@ export default function Layout(props: ParentProps) {
} }
const openSession = async (target: { directory: string; id: string }) => { const openSession = async (target: { directory: string; id: string }) => {
if (!canOpen(target.directory)) return false if (!canOpen(target.directory)) return false
const [data] = globalSync.child(target.directory, { bootstrap: false }) const [data] = serverSync.child(target.directory, { bootstrap: false })
if (data.session.some((item) => item.id === target.id)) { if (data.session.some((item) => item.id === target.id)) {
setStore("lastProjectSession", root, { directory: target.directory, id: target.id, at: Date.now() }) setStore("lastProjectSession", root, { directory: target.directory, id: target.id, at: Date.now() })
navigateWithSidebarReset(`/${base64Encode(target.directory)}/session/${target.id}`) navigateWithSidebarReset(`/${base64Encode(target.directory)}/session/${target.id}`)
return true return true
} }
const resolved = await globalSDK.client.session const resolved = await serverSDK.client.session
.get({ sessionID: target.id }) .get({ sessionID: target.id })
.then((x) => x.data) .then((x) => x.data)
.catch(() => undefined) .catch(() => undefined)
@@ -1331,7 +1332,7 @@ export default function Layout(props: ParentProps) {
} }
const latest = latestRootSession( const latest = latestRootSession(
dirs.map((item) => globalSync.child(item, { bootstrap: false })[0]), dirs.map((item) => serverSync.child(item, { bootstrap: false })[0]),
Date.now(), Date.now(),
) )
if (latest && (await openSession(latest))) { if (latest && (await openSession(latest))) {
@@ -1342,7 +1343,7 @@ export default function Layout(props: ParentProps) {
await Promise.all( await Promise.all(
dirs.map(async (item) => ({ dirs.map(async (item) => ({
path: { directory: item }, path: { directory: item },
session: await globalSDK.client.session session: await serverSDK.client.session
.list({ directory: item }) .list({ directory: item })
.then((x) => x.data ?? []) .then((x) => x.data ?? [])
.catch(() => []), .catch(() => []),
@@ -1403,11 +1404,11 @@ export default function Layout(props: ParentProps) {
const name = next === getFilename(project.worktree) ? "" : next const name = next === getFilename(project.worktree) ? "" : next
if (project.id && project.id !== "global") { if (project.id && project.id !== "global") {
await globalSDK.client.project.update({ projectID: project.id, directory: project.worktree, name }) await serverSDK.client.project.update({ projectID: project.id, directory: project.worktree, name })
return return
} }
globalSync.project.meta(project.worktree, { name }) serverSync.project.meta(project.worktree, { name })
} }
const renameWorkspace = (directory: string, next: string, projectId?: string, branch?: string) => { const renameWorkspace = (directory: string, next: string, projectId?: string, branch?: string) => {
@@ -1504,7 +1505,7 @@ export default function Layout(props: ParentProps) {
setBusy(directory, true) setBusy(directory, true)
const result = await globalSDK.client.worktree const result = await serverSDK.client.worktree
.remove({ directory: root, worktreeRemoveInput: { directory } }) .remove({ directory: root, worktreeRemoveInput: { directory } })
.then((x) => x.data) .then((x) => x.data)
.catch((err) => { .catch((err) => {
@@ -1523,7 +1524,7 @@ export default function Layout(props: ParentProps) {
clearLastProjectSession(root) clearLastProjectSession(root)
} }
globalSync.set( serverSync.set(
"project", "project",
produce((draft) => { produce((draft) => {
const project = draft.find((item) => item.worktree === root) const project = draft.find((item) => item.worktree === root)
@@ -1562,7 +1563,7 @@ export default function Layout(props: ParentProps) {
}) })
const dismiss = () => toaster.dismiss(progress) const dismiss = () => toaster.dismiss(progress)
const sessions: Session[] = await globalSDK.client.session const sessions: Session[] = await serverSDK.client.session
.list({ directory }) .list({ directory })
.then((x) => x.data ?? []) .then((x) => x.data ?? [])
.catch(() => []) .catch(() => [])
@@ -1573,9 +1574,9 @@ export default function Layout(props: ParentProps) {
platform, platform,
getTerminalServerScope(server.current, server.key), getTerminalServerScope(server.current, server.key),
) )
await globalSDK.client.instance.dispose({ directory }).catch(() => undefined) await serverSDK.client.instance.dispose({ directory }).catch(() => undefined)
const result = await globalSDK.client.worktree const result = await serverSDK.client.worktree
.reset({ directory: root, worktreeResetInput: { directory } }) .reset({ directory: root, worktreeResetInput: { directory } })
.then((x) => x.data) .then((x) => x.data)
.catch((err) => { .catch((err) => {
@@ -1597,7 +1598,7 @@ export default function Layout(props: ParentProps) {
sessions sessions
.filter((session) => session.time.archived === undefined) .filter((session) => session.time.archived === undefined)
.map((session) => .map((session) =>
globalSDK.client.session serverSDK.client.session
.update({ .update({
sessionID: session.id, sessionID: session.id,
directory: session.directory, directory: session.directory,
@@ -1638,7 +1639,7 @@ export default function Layout(props: ParentProps) {
}) })
onMount(() => { onMount(() => {
globalSDK.client.file serverSDK.client.file
.status({ directory: props.directory }) .status({ directory: props.directory })
.then((x) => { .then((x) => {
const files = x.data ?? [] const files = x.data ?? []
@@ -1697,7 +1698,7 @@ export default function Layout(props: ParentProps) {
}) })
const refresh = async () => { const refresh = async () => {
const sessions = await globalSDK.client.session const sessions = await serverSDK.client.session
.list({ directory: props.directory }) .list({ directory: props.directory })
.then((x) => x.data ?? []) .then((x) => x.data ?? [])
.catch(() => []) .catch(() => [])
@@ -1706,7 +1707,7 @@ export default function Layout(props: ParentProps) {
} }
onMount(() => { onMount(() => {
globalSDK.client.file serverSDK.client.file
.status({ directory: props.directory }) .status({ directory: props.directory })
.then((x) => { .then((x) => {
const files = x.data ?? [] const files = x.data ?? []
@@ -1819,7 +1820,7 @@ export default function Layout(props: ParentProps) {
createEffect(() => { createEffect(() => {
document.documentElement.style.setProperty( document.documentElement.style.setProperty(
"--dialog-left-margin", "--dialog-left-margin",
USE_HOME_DESIGN ? "0px" : `${layout.sidebar.opened() ? layout.sidebar.width() : 48}px`, newDesign() ? "0px" : `${layout.sidebar.opened() ? layout.sidebar.width() : 48}px`,
) )
}) })
@@ -1840,7 +1841,7 @@ export default function Layout(props: ParentProps) {
const next = new Set(dirs) const next = new Set(dirs)
for (const directory of next) { for (const directory of next) {
if (loadedSessionDirs.has(directory)) continue if (loadedSessionDirs.has(directory)) continue
void globalSync.project.loadSessions(directory) void serverSync.project.loadSessions(directory)
} }
loadedSessionDirs.clear() loadedSessionDirs.clear()
@@ -1937,7 +1938,7 @@ export default function Layout(props: ParentProps) {
const createWorkspace = async (project: LocalProject) => { const createWorkspace = async (project: LocalProject) => {
clearSidebarHoverState() clearSidebarHoverState()
const created = await globalSDK.client.worktree const created = await serverSDK.client.worktree
.create({ directory: project.worktree }) .create({ directory: project.worktree })
.then((x) => x.data) .then((x) => x.data)
.catch((err) => { .catch((err) => {
@@ -1971,7 +1972,7 @@ export default function Layout(props: ParentProps) {
return [created.directory, ...next] return [created.directory, ...next]
}) })
globalSync.child(created.directory) serverSync.child(created.directory)
navigateWithSidebarReset(`/${base64Encode(created.directory)}/session`) navigateWithSidebarReset(`/${base64Encode(created.directory)}/session`)
} }
@@ -2076,7 +2077,7 @@ export default function Layout(props: ParentProps) {
if (!item) return false if (!item) return false
return item.vcs === "git" || layout.sidebar.workspaces(item.worktree)() return item.vcs === "git" || layout.sidebar.workspaces(item.worktree)()
}) })
const homedir = createMemo(() => globalSync.data.path.home) const homedir = createMemo(() => serverSync.data.path.home)
return ( return (
<div <div
@@ -2303,7 +2304,7 @@ export default function Layout(props: ParentProps) {
<div <div
class="shrink-0 px-3 py-3" class="shrink-0 px-3 py-3"
classList={{ classList={{
hidden: store.gettingStartedDismissed || !(providers.all().length > 0 && providers.paid().length === 0), hidden: store.gettingStartedDismissed || !(providers.all().size > 0 && providers.paid().length === 0),
}} }}
> >
<div class="rounded-xl bg-background-base shadow-xs-border-base" data-component="getting-started"> <div class="rounded-xl bg-background-base shadow-xs-border-base" data-component="getting-started">
@@ -2361,179 +2362,180 @@ export default function Layout(props: ParentProps) {
/> />
) )
if (USE_HOME_DESIGN) { return (
return ( <Show
<div class="relative bg-v2-background-bg-deep flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text"> when={!newDesign()}
fallback={
<div class="relative bg-v2-background-bg-deep flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
{autoselecting() ?? ""}
<Titlebar update={titlebarUpdate} />
<main
class="flex-1 min-h-0 min-w-0 overflow-x-hidden flex flex-col items-start contain-strict bg-v2-background-bg-base"
classList={{
"m-2 mt-0 rounded-[10px] shadow-[var(--v2-elevation-raised)] overflow-hidden": !!params.id || !params.dir,
}}
>
<Show when={!autoselecting.loading} fallback={<div class="size-full" />}>
{props.children}
</Show>
</main>
{import.meta.env.DEV && <DebugBar />}
<Toast.Region />
</div>
}
>
<div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
{autoselecting() ?? ""} {autoselecting() ?? ""}
<Titlebar update={titlebarUpdate} /> <Titlebar update={titlebarUpdate} />
<main <Show when={updateVersion() !== undefined}>
class="flex-1 min-h-0 min-w-0 overflow-x-hidden flex flex-col items-start contain-strict bg-v2-background-bg-base" <UpdateAvailableToast version={updateVersion() ?? ""} install={installUpdate} language={language} />
classList={{ </Show>
"m-2 mt-0 rounded-[10px] shadow-[var(--v2-elevation-raised)] overflow-hidden": !!params.id || !params.dir, <div class="flex-1 min-h-0 min-w-0 flex">
}} <div class="flex-1 min-h-0 relative">
> <div class="size-full relative overflow-x-hidden">
<Show when={!autoselecting.loading} fallback={<div class="size-full" />}>
{props.children}
</Show>
</main>
{import.meta.env.DEV && <DebugBar />}
<Toast.Region />
</div>
)
}
return (
<div class="relative bg-background-base flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text">
{autoselecting() ?? ""}
<Titlebar update={titlebarUpdate} />
<Show when={updateVersion() !== undefined}>
<UpdateAvailableToast version={updateVersion() ?? ""} install={installUpdate} language={language} />
</Show>
<div class="flex-1 min-h-0 min-w-0 flex">
<div class="flex-1 min-h-0 relative">
<div class="size-full relative overflow-x-hidden">
<nav
aria-label={language.t("sidebar.nav.projectsAndSessions")}
data-component="sidebar-nav-desktop"
classList={{
"hidden xl:block": true,
"absolute inset-y-0 left-0": true,
"z-10": true,
}}
style={{ width: `${side()}px` }}
ref={(el) => {
setState("nav", el)
}}
onMouseEnter={() => {
disarm()
}}
onMouseLeave={() => {
aim.reset()
if (!sidebarHovering()) return
arm()
}}
>
<div class="@container w-full h-full contain-strict">{sidebarContent()}</div>
</nav>
<Show when={layout.sidebar.opened()}>
<div
class="hidden xl:block absolute inset-y-0 z-30 w-0 overflow-visible"
style={{ left: `${side()}px` }}
onPointerDown={() => setState("sizing", true)}
>
<ResizeHandle
direction="horizontal"
size={layout.sidebar.width()}
min={244}
max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64}
onResize={(w) => {
setState("sizing", true)
if (sizet !== undefined) clearTimeout(sizet)
sizet = window.setTimeout(() => setState("sizing", false), 120)
layout.sidebar.resize(w)
}}
/>
</div>
</Show>
<div
class="hidden xl:block pointer-events-none absolute top-0 right-0 z-0 border-t border-border-weaker-base"
style={{ left: "calc(4rem + 12px)" }}
/>
<div class="xl:hidden">
<div
classList={{
"fixed inset-x-0 top-10 bottom-0 z-40 transition-opacity duration-200": true,
"opacity-100 pointer-events-auto": layout.mobileSidebar.opened(),
"opacity-0 pointer-events-none": !layout.mobileSidebar.opened(),
}}
onClick={(e) => {
if (e.target === e.currentTarget) layout.mobileSidebar.hide()
}}
/>
<nav <nav
aria-label={language.t("sidebar.nav.projectsAndSessions")} aria-label={language.t("sidebar.nav.projectsAndSessions")}
data-component="sidebar-nav-mobile" data-component="sidebar-nav-desktop"
classList={{ classList={{
"@container fixed top-10 bottom-0 left-0 z-50 w-full max-w-[400px] overflow-hidden border-r border-border-weaker-base bg-background-base transition-transform duration-200 ease-out": true, "hidden xl:block": true,
"translate-x-0": layout.mobileSidebar.opened(), "absolute inset-y-0 left-0": true,
"-translate-x-full": !layout.mobileSidebar.opened(), "z-10": true,
}}
style={{ width: `${side()}px` }}
ref={(el) => {
setState("nav", el)
}}
onMouseEnter={() => {
disarm()
}}
onMouseLeave={() => {
aim.reset()
if (!sidebarHovering()) return
arm()
}} }}
onClick={(e) => e.stopPropagation()}
> >
{sidebarContent(true)} <div class="@container w-full h-full contain-strict">{sidebarContent()}</div>
</nav> </nav>
</div>
<div <Show when={layout.sidebar.opened()}>
classList={{ <div
"absolute inset-0": true, class="hidden xl:block absolute inset-y-0 z-30 w-0 overflow-visible"
"xl:inset-y-0 xl:right-0 xl:left-[var(--main-left)]": true, style={{ left: `${side()}px` }}
"z-20": true, onPointerDown={() => setState("sizing", true)}
"transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none": >
!state.sizing, <ResizeHandle
}} direction="horizontal"
style={{ size={layout.sidebar.width()}
"--main-left": layout.sidebar.opened() ? `${side()}px` : "4rem", min={244}
}} max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64}
> onResize={(w) => {
<main setState("sizing", true)
if (sizet !== undefined) clearTimeout(sizet)
sizet = window.setTimeout(() => setState("sizing", false), 120)
layout.sidebar.resize(w)
}}
/>
</div>
</Show>
<div
class="hidden xl:block pointer-events-none absolute top-0 right-0 z-0 border-t border-border-weaker-base"
style={{ left: "calc(4rem + 12px)" }}
/>
<div class="xl:hidden">
<div
classList={{
"fixed inset-x-0 top-10 bottom-0 z-40 transition-opacity duration-200": true,
"opacity-100 pointer-events-auto": layout.mobileSidebar.opened(),
"opacity-0 pointer-events-none": !layout.mobileSidebar.opened(),
}}
onClick={(e) => {
if (e.target === e.currentTarget) layout.mobileSidebar.hide()
}}
/>
<nav
aria-label={language.t("sidebar.nav.projectsAndSessions")}
data-component="sidebar-nav-mobile"
classList={{
"@container fixed top-10 bottom-0 left-0 z-50 w-full max-w-[400px] overflow-hidden border-r border-border-weaker-base bg-background-base transition-transform duration-200 ease-out": true,
"translate-x-0": layout.mobileSidebar.opened(),
"-translate-x-full": !layout.mobileSidebar.opened(),
}}
onClick={(e) => e.stopPropagation()}
>
{sidebarContent(true)}
</nav>
</div>
<div
classList={{ classList={{
"size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base bg-background-base xl:border-l xl:rounded-tl-[12px]": true, "absolute inset-0": true,
"xl:inset-y-0 xl:right-0 xl:left-[var(--main-left)]": true,
"z-20": true,
"transition-[left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[left] motion-reduce:transition-none":
!state.sizing,
}}
style={{
"--main-left": layout.sidebar.opened() ? `${side()}px` : "4rem",
}} }}
> >
<Show when={!autoselecting.loading} fallback={<div class="size-full" />}> <main
{props.children} classList={{
"size-full overflow-x-hidden flex flex-col items-start contain-strict border-t border-border-weak-base bg-background-base xl:border-l xl:rounded-tl-[12px]": true,
}}
>
<Show when={!autoselecting.loading} fallback={<div class="size-full" />}>
{props.children}
</Show>
</main>
</div>
<div
classList={{
"hidden xl:flex absolute inset-y-0 left-16 z-30": true,
"opacity-100 translate-x-0 pointer-events-auto": state.peeked && !layout.sidebar.opened(),
"opacity-0 -translate-x-2 pointer-events-none": !state.peeked || layout.sidebar.opened(),
"transition-[opacity,transform] motion-reduce:transition-none": true,
"duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
"duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
}}
onMouseMove={disarm}
onMouseEnter={() => {
disarm()
aim.reset()
}}
onPointerDown={disarm}
onMouseLeave={() => {
arm()
}}
>
<Show when={peekProject()}>
<SidebarPanel project={peekProject} merged={false} />
</Show> </Show>
</main> </div>
</div>
<div <div
classList={{ classList={{
"hidden xl:flex absolute inset-y-0 left-16 z-30": true, "hidden xl:block pointer-events-none absolute inset-y-0 right-0 z-25 overflow-hidden": true,
"opacity-100 translate-x-0 pointer-events-auto": state.peeked && !layout.sidebar.opened(), "opacity-100 translate-x-0": state.peeked && !layout.sidebar.opened(),
"opacity-0 -translate-x-2 pointer-events-none": !state.peeked || layout.sidebar.opened(), "opacity-0 -translate-x-2": !state.peeked || layout.sidebar.opened(),
"transition-[opacity,transform] motion-reduce:transition-none": true, "transition-[opacity,transform] motion-reduce:transition-none": true,
"duration-180 ease-out": state.peeked && !layout.sidebar.opened(), "duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
"duration-120 ease-in": !state.peeked || layout.sidebar.opened(), "duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
}} }}
onMouseMove={disarm} style={{ left: `calc(4rem + ${panel()}px)` }}
onMouseEnter={() => { >
disarm() <div class="h-full w-px" style={{ "box-shadow": "var(--shadow-sidebar-overlay)" }} />
aim.reset() </div>
}}
onPointerDown={disarm}
onMouseLeave={() => {
arm()
}}
>
<Show when={peekProject()}>
<SidebarPanel project={peekProject} merged={false} />
</Show>
</div>
<div
classList={{
"hidden xl:block pointer-events-none absolute inset-y-0 right-0 z-25 overflow-hidden": true,
"opacity-100 translate-x-0": state.peeked && !layout.sidebar.opened(),
"opacity-0 -translate-x-2": !state.peeked || layout.sidebar.opened(),
"transition-[opacity,transform] motion-reduce:transition-none": true,
"duration-180 ease-out": state.peeked && !layout.sidebar.opened(),
"duration-120 ease-in": !state.peeked || layout.sidebar.opened(),
}}
style={{ left: `calc(4rem + ${panel()}px)` }}
>
<div class="h-full w-px" style={{ "box-shadow": "var(--shadow-sidebar-overlay)" }} />
</div> </div>
</div> </div>
{import.meta.env.DEV && <DebugBar />}
</div> </div>
{import.meta.env.DEV && <DebugBar />} <Toast.Region />
</div> </div>
<Toast.Region /> </Show>
</div>
) )
} }
@@ -7,7 +7,7 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
import { getFilename } from "@opencode-ai/core/util/path" import { getFilename } from "@opencode-ai/core/util/path"
import { A, useParams } from "@solidjs/router" import { A, useParams } from "@solidjs/router"
import { type Accessor, createMemo, For, type JSX, Match, Show, Switch } from "solid-js" import { type Accessor, createMemo, For, type JSX, Match, Show, Switch } from "solid-js"
import { useGlobalSync } from "@/context/global-sync" import { useServerSync } from "@/context/server-sync"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout" import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout"
import { useNotification } from "@/context/notification" import { useNotification } from "@/context/notification"
@@ -23,7 +23,7 @@ export const ProjectIcon = (props: {
notify?: boolean notify?: boolean
working?: boolean working?: boolean
}): JSX.Element => { }): JSX.Element => {
const globalSync = useGlobalSync() const serverSync = useServerSync()
const notification = useNotification() const notification = useNotification()
const permission = usePermission() const permission = usePermission()
const dirs = createMemo(() => [props.project.worktree, ...(props.project.sandboxes ?? [])]) const dirs = createMemo(() => [props.project.worktree, ...(props.project.sandboxes ?? [])])
@@ -33,7 +33,7 @@ export const ProjectIcon = (props: {
const hasError = createMemo(() => dirs().some((directory) => notification.project.unseenHasError(directory))) const hasError = createMemo(() => dirs().some((directory) => notification.project.unseenHasError(directory)))
const hasPermissions = createMemo(() => const hasPermissions = createMemo(() =>
dirs().some((directory) => { dirs().some((directory) => {
const [store] = globalSync.child(directory, { bootstrap: false }) const [store] = serverSync.child(directory, { bootstrap: false })
return hasProjectPermissions(store.permission, (item) => !permission.autoResponds(item, directory)) return hasProjectPermissions(store.permission, (item) => !permission.autoResponds(item, directory))
}), }),
) )
@@ -146,10 +146,10 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
const language = useLanguage() const language = useLanguage()
const notification = useNotification() const notification = useNotification()
const permission = usePermission() const permission = usePermission()
const globalSync = useGlobalSync() const serverSync = useServerSync()
const unseenCount = createMemo(() => notification.session.unseenCount(props.session.id)) const unseenCount = createMemo(() => notification.session.unseenCount(props.session.id))
const hasError = createMemo(() => notification.session.unseenHasError(props.session.id)) const hasError = createMemo(() => notification.session.unseenHasError(props.session.id))
const [sessionStore] = globalSync.child(props.session.directory) const [sessionStore] = serverSync.child(props.session.directory)
const hasPermissions = createMemo(() => { const hasPermissions = createMemo(() => {
return !!sessionPermissionRequest(sessionStore.session, sessionStore.permission, props.session.id, (item) => { return !!sessionPermissionRequest(sessionStore.session, sessionStore.permission, props.session.id, (item) => {
return !permission.autoResponds(item, props.session.directory) return !permission.autoResponds(item, props.session.directory)
@@ -7,7 +7,7 @@ import { HoverCard } from "@opencode-ai/ui/hover-card"
import { Icon } from "@opencode-ai/ui/icon" import { Icon } from "@opencode-ai/ui/icon"
import { createSortable } from "@thisbeyond/solid-dnd" import { createSortable } from "@thisbeyond/solid-dnd"
import { useLayout, type LocalProject } from "@/context/layout" import { useLayout, type LocalProject } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync" import { useServerSync } from "@/context/server-sync"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useNotification } from "@/context/notification" import { useNotification } from "@/context/notification"
import { ProjectIcon, SessionItem, type SessionItemProps } from "./sidebar-items" import { ProjectIcon, SessionItem, type SessionItemProps } from "./sidebar-items"
@@ -274,7 +274,7 @@ export const SortableProject = (props: {
ctx: ProjectSidebarContext ctx: ProjectSidebarContext
sortNow: Accessor<number> sortNow: Accessor<number>
}): JSX.Element => { }): JSX.Element => {
const globalSync = useGlobalSync() const serverSync = useServerSync()
const language = useLanguage() const language = useLanguage()
const sortable = createSortable(props.project.worktree) const sortable = createSortable(props.project.worktree)
const selected = createMemo(() => props.ctx.currentProject()?.worktree === props.project.worktree) const selected = createMemo(() => props.ctx.currentProject()?.worktree === props.project.worktree)
@@ -294,23 +294,23 @@ export const SortableProject = (props: {
const hoverOpen = () => isHoverProject() && preview() && !selected() && !state.menu const hoverOpen = () => isHoverProject() && preview() && !selected() && !state.menu
const label = (directory: string) => { const label = (directory: string) => {
const [data] = globalSync.child(directory, { bootstrap: false }) const [data] = serverSync.child(directory, { bootstrap: false })
const kind = const kind =
directory === props.project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox") directory === props.project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")
const name = props.ctx.workspaceLabel(directory, data.vcs?.branch, props.project.id) const name = props.ctx.workspaceLabel(directory, data.vcs?.branch, props.project.id)
return `${kind} : ${name}` return `${kind} : ${name}`
} }
const projectStore = createMemo(() => globalSync.child(props.project.worktree, { bootstrap: false })[0]) const projectStore = createMemo(() => serverSync.child(props.project.worktree, { bootstrap: false })[0])
const isWorking = createMemo(() => const isWorking = createMemo(() =>
dirs().some((directory) => { dirs().some((directory) => {
const [store] = globalSync.child(directory, { bootstrap: false }) const [store] = serverSync.child(directory, { bootstrap: false })
return Object.keys(store.session_status).some((id) => store.session_working(id)) return Object.keys(store.session_status).some((id) => store.session_working(id))
}), }),
) )
const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow())) const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow()))
const workspaceSessions = (directory: string) => { const workspaceSessions = (directory: string) => {
const [data] = globalSync.child(directory, { bootstrap: false }) const [data] = serverSync.child(directory, { bootstrap: false })
return sortedRootSessions(data, props.sortNow()) return sortedRootSessions(data, props.sortNow())
} }
const tile = () => ( const tile = () => (
@@ -14,7 +14,7 @@ import { Spinner } from "@opencode-ai/ui/spinner"
import { Tooltip } from "@opencode-ai/ui/tooltip" import { Tooltip } from "@opencode-ai/ui/tooltip"
import { type Session } from "@opencode-ai/sdk/v2/client" import { type Session } from "@opencode-ai/sdk/v2/client"
import { type LocalProject } from "@/context/layout" import { type LocalProject } from "@/context/layout"
import { useGlobalSync, useQueryOptions } from "@/context/global-sync" import { useServerSync, useQueryOptions } from "@/context/server-sync"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { pathKey } from "@/utils/path-key" import { pathKey } from "@/utils/path-key"
import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items" import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
@@ -60,7 +60,7 @@ export const WorkspaceDragOverlay = (props: {
activeWorkspace: Accessor<string | undefined> activeWorkspace: Accessor<string | undefined>
workspaceLabel: (directory: string, branch?: string, projectId?: string) => string workspaceLabel: (directory: string, branch?: string, projectId?: string) => string
}): JSX.Element => { }): JSX.Element => {
const globalSync = useGlobalSync() const serverSync = useServerSync()
const language = useLanguage() const language = useLanguage()
const label = createMemo(() => { const label = createMemo(() => {
const project = props.sidebarProject() const project = props.sidebarProject()
@@ -68,7 +68,7 @@ export const WorkspaceDragOverlay = (props: {
const directory = props.activeWorkspace() const directory = props.activeWorkspace()
if (!directory) return if (!directory) return
const [workspaceStore] = globalSync.child(directory, { bootstrap: false }) const [workspaceStore] = serverSync.child(directory, { bootstrap: false })
const kind = const kind =
directory === project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox") directory === project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")
const name = props.workspaceLabel(directory, workspaceStore.vcs?.branch, project.id) const name = props.workspaceLabel(directory, workspaceStore.vcs?.branch, project.id)
@@ -299,11 +299,11 @@ export const SortableWorkspace = (props: {
}): JSX.Element => { }): JSX.Element => {
const navigate = useNavigate() const navigate = useNavigate()
const params = useParams() const params = useParams()
const globalSync = useGlobalSync() const serverSync = useServerSync()
const queryOptions = useQueryOptions() const queryOptions = useQueryOptions()
const language = useLanguage() const language = useLanguage()
const sortable = createSortable(props.directory) const sortable = createSortable(props.directory)
const [workspaceStore, setWorkspaceStore] = globalSync.child(props.directory, { bootstrap: false }) const [workspaceStore, setWorkspaceStore] = serverSync.child(props.directory, { bootstrap: false })
const [menu, setMenu] = createStore({ const [menu, setMenu] = createStore({
open: false, open: false,
pendingRename: false, pendingRename: false,
@@ -328,7 +328,7 @@ export const SortableWorkspace = (props: {
const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id))) const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id)))
const loadMore = async () => { const loadMore = async () => {
setWorkspaceStore("limit", (limit) => (limit ?? 0) + 5) setWorkspaceStore("limit", (limit) => (limit ?? 0) + 5)
await globalSync.project.loadSessions(props.directory) await serverSync.project.loadSessions(props.directory)
} }
const workspaceEditActive = createMemo(() => props.ctx.editorOpen(`workspace:${props.directory}`)) const workspaceEditActive = createMemo(() => props.ctx.editorOpen(`workspace:${props.directory}`))
@@ -357,7 +357,7 @@ export const SortableWorkspace = (props: {
createEffect(() => { createEffect(() => {
if (!boot()) return if (!boot()) return
globalSync.child(props.directory, { bootstrap: true }) serverSync.child(props.directory, { bootstrap: true })
}) })
return ( return (
@@ -446,11 +446,11 @@ export const LocalWorkspace = (props: {
sortNow: Accessor<number> sortNow: Accessor<number>
mobile?: boolean mobile?: boolean
}): JSX.Element => { }): JSX.Element => {
const globalSync = useGlobalSync() const serverSync = useServerSync()
const queryOptions = useQueryOptions() const queryOptions = useQueryOptions()
const language = useLanguage() const language = useLanguage()
const workspace = createMemo(() => { const workspace = createMemo(() => {
const [store, setStore] = globalSync.child(props.project.worktree) const [store, setStore] = serverSync.child(props.project.worktree)
return { store, setStore } return { store, setStore }
}) })
const slug = createMemo(() => base64Encode(props.project.worktree)) const slug = createMemo(() => base64Encode(props.project.worktree))
@@ -461,7 +461,7 @@ export const LocalWorkspace = (props: {
const loading = () => fetching() > 0 && count() === 0 const loading = () => fetching() > 0 && count() === 0
const loadMore = async () => { const loadMore = async () => {
workspace().setStore("limit", (limit) => (limit ?? 0) + 5) workspace().setStore("limit", (limit) => (limit ?? 0) + 5)
await globalSync.project.loadSessions(props.project.worktree) await serverSync.project.loadSessions(props.project.worktree)
} }
return ( return (
+21 -20
View File
@@ -18,6 +18,7 @@ import {
import { makeEventListener } from "@solid-primitives/event-listener" import { makeEventListener } from "@solid-primitives/event-listener"
import { createMediaQuery } from "@solid-primitives/media" import { createMediaQuery } from "@solid-primitives/media"
import { createResizeObserver } from "@solid-primitives/resize-observer" import { createResizeObserver } from "@solid-primitives/resize-observer"
import { debounce } from "@solid-primitives/scheduled"
import { useLocal } from "@/context/local" import { useLocal } from "@/context/local"
import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file" import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
@@ -33,7 +34,7 @@ import { useLocation, useSearchParams } from "@solidjs/router"
import { NewSessionDesignView, NewSessionView, SessionHeader } from "@/components/session" import { NewSessionDesignView, NewSessionView, SessionHeader } from "@/components/session"
import { useComments } from "@/context/comments" import { useComments } from "@/context/comments"
import { getSessionPrefetch, SESSION_PREFETCH_TTL } from "@/context/global-sync/session-prefetch" import { getSessionPrefetch, SESSION_PREFETCH_TTL } from "@/context/global-sync/session-prefetch"
import { useGlobalSync } from "@/context/global-sync" import { useServerSync } from "@/context/server-sync"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout" import { useLayout } from "@/context/layout"
import { usePrompt } from "@/context/prompt" import { usePrompt } from "@/context/prompt"
@@ -58,6 +59,7 @@ import { SessionSidePanel } from "@/pages/session/session-side-panel"
import { TerminalPanel } from "@/pages/session/terminal-panel" import { TerminalPanel } from "@/pages/session/terminal-panel"
import { useSessionCommands } from "@/pages/session/use-session-commands" import { useSessionCommands } from "@/pages/session/use-session-commands"
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll" import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
import { shouldUseV2NewSessionPage } from "@/pages/session/new-session-layout"
import { Identifier } from "@/utils/id" import { Identifier } from "@/utils/id"
import { diffs as list } from "@/utils/diffs" import { diffs as list } from "@/utils/diffs"
import { Persist, persisted } from "@/utils/persist" import { Persist, persisted } from "@/utils/persist"
@@ -73,7 +75,6 @@ const emptyFollowups: FollowupItem[] = []
type ChangeMode = "git" | "branch" | "turn" type ChangeMode = "git" | "branch" | "turn"
type VcsMode = "git" | "branch" type VcsMode = "git" | "branch"
const USE_NEW_SESSION_DESIGN = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod"
type SessionHistoryWindowInput = { type SessionHistoryWindowInput = {
sessionID: () => string | undefined sessionID: () => string | undefined
@@ -180,7 +181,7 @@ function createSessionHistoryLoader(input: SessionHistoryWindowInput) {
} }
export default function Page() { export default function Page() {
const globalSync = useGlobalSync() const serverSync = useServerSync()
const layout = useLayout() const layout = useLayout()
const local = useLocal() const local = useLocal()
const file = useFile() const file = useFile()
@@ -196,6 +197,7 @@ export default function Page() {
const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>() const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
const location = useLocation() const location = useLocation()
const { params, sessionKey, tabs, view } = useSessionLayout() const { params, sessionKey, tabs, view } = useSessionLayout()
const newSessionDesign = createMemo(() => settings.general.newLayoutDesigns())
createEffect(() => { createEffect(() => {
if (!prompt.ready()) return if (!prompt.ready()) return
@@ -262,8 +264,10 @@ export default function Page() {
const isDesktop = createMediaQuery("(min-width: 768px)") const isDesktop = createMediaQuery("(min-width: 768px)")
const size = createSizing() const size = createSizing()
const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) const isV2NewSessionPage = () =>
const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened()) shouldUseV2NewSessionPage({ newLayoutDesigns: newSessionDesign(), sessionID: params.id })
const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened() && !isV2NewSessionPage())
const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened() && !isV2NewSessionPage())
const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen()) const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen())
const sessionPanelWidth = createMemo(() => { const sessionPanelWidth = createMemo(() => {
if (!desktopSidePanelOpen()) return "100%" if (!desktopSidePanelOpen()) return "100%"
@@ -477,7 +481,7 @@ export default function Page() {
: skipToken, : skipToken,
} }
}) })
const refreshVcs = () => void queryClient.invalidateQueries({ queryKey: vcsKey() }) const refreshVcs = debounce(() => void queryClient.invalidateQueries({ queryKey: vcsKey() }), 100)
const reviewDiffs = () => { const reviewDiffs = () => {
if (store.changes === "git" || store.changes === "branch") if (store.changes === "git" || store.changes === "branch")
// avoids suspense // avoids suspense
@@ -556,11 +560,11 @@ export default function Page() {
} }
function upsert(next: Project) { function upsert(next: Project) {
const list = globalSync.data.project const list = serverSync.data.project
sync.set("project", next.id) sync.set("project", next.id)
const idx = list.findIndex((item) => item.id === next.id) const idx = list.findIndex((item) => item.id === next.id)
if (idx >= 0) { if (idx >= 0) {
globalSync.set( serverSync.set(
"project", "project",
list.map((item, i) => (i === idx ? { ...item, ...next } : item)), list.map((item, i) => (i === idx ? { ...item, ...next } : item)),
) )
@@ -568,10 +572,10 @@ export default function Page() {
} }
const at = list.findIndex((item) => item.id > next.id) const at = list.findIndex((item) => item.id > next.id)
if (at >= 0) { if (at >= 0) {
globalSync.set("project", [...list.slice(0, at), next, ...list.slice(at)]) serverSync.set("project", [...list.slice(0, at), next, ...list.slice(at)])
return return
} }
globalSync.set("project", [...list, next]) serverSync.set("project", [...list, next])
} }
const gitMutation = useMutation(() => ({ const gitMutation = useMutation(() => ({
@@ -669,7 +673,7 @@ export default function Page() {
todoTimer = undefined todoTimer = undefined
if (!id) return if (!id) return
if (status === "idle" && !blocked) return if (status === "idle" && !blocked) return
const cached = untrack(() => sync.data.todo[id] !== undefined || globalSync.data.session_todo[id] !== undefined) const cached = untrack(() => sync.data.todo[id] !== undefined || serverSync.data.session_todo[id] !== undefined)
todoFrame = requestAnimationFrame(() => { todoFrame = requestAnimationFrame(() => {
todoFrame = undefined todoFrame = undefined
@@ -1382,7 +1386,7 @@ export default function Page() {
const ok = await sendFollowupDraft({ const ok = await sendFollowupDraft({
client: sdk.client, client: sdk.client,
sync, sync,
globalSync, serverSync,
draft: item, draft: item,
optimisticBusy: item.sessionDirectory === sdk.directory, optimisticBusy: item.sessionDirectory === sdk.directory,
}).catch((err) => { }).catch((err) => {
@@ -1659,7 +1663,6 @@ export default function Page() {
inputRef = el inputRef = el
}} }}
newSessionWorktree={newSessionWorktree()} newSessionWorktree={newSessionWorktree()}
onNewSessionWorktreeChange={(value) => setStore("newSessionWorktree", value)}
onNewSessionWorktreeReset={() => setStore("newSessionWorktree", "main")} onNewSessionWorktreeReset={() => setStore("newSessionWorktree", "main")}
onSubmit={() => { onSubmit={() => {
comments.clear() comments.clear()
@@ -1733,12 +1736,12 @@ export default function Page() {
</Tabs> </Tabs>
</Show> </Show>
{/* Session panel */}
<div <div
classList={{ classList={{
"@container relative shrink-0 flex flex-col min-h-0 h-full bg-background-stronger flex-1 md:flex-none": true, "@container relative shrink-0 flex flex-col min-h-0 h-full bg-background-stronger flex-1 md:flex-none": true,
"transition-[width] duration-[240ms] ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none": "duration-[240ms] ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width] motion-reduce:transition-none":
!size.active() && !ui.reviewSnap, !size.active() && !ui.reviewSnap,
"transition-[width]": !isV2NewSessionPage(),
}} }}
style={{ style={{
width: sessionPanelWidth(), width: sessionPanelWidth(),
@@ -1795,16 +1798,14 @@ export default function Page() {
</Show> </Show>
</Match> </Match>
<Match when={true}> <Match when={true}>
<Show when={USE_NEW_SESSION_DESIGN} fallback={<NewSessionView worktree={newSessionWorktree()} />}> <Show when={newSessionDesign()} fallback={<NewSessionView worktree={newSessionWorktree()} />}>
<NewSessionDesignView worktree={newSessionWorktree()}> <NewSessionDesignView>{composerRegion("inline")}</NewSessionDesignView>
{composerRegion("inline")}
</NewSessionDesignView>
</Show> </Show>
</Match> </Match>
</Switch> </Switch>
</div> </div>
<Show when={params.id || !USE_NEW_SESSION_DESIGN}>{composerRegion("dock")}</Show> <Show when={params.id || !newSessionDesign()}>{composerRegion("dock")}</Show>
<Show when={desktopReviewOpen()}> <Show when={desktopReviewOpen()}>
<div onPointerDown={() => size.start()}> <div onPointerDown={() => size.start()}>
@@ -25,7 +25,6 @@ export function SessionComposerRegion(props: {
placement?: "dock" | "inline" placement?: "dock" | "inline"
inputRef: (el: HTMLDivElement) => void inputRef: (el: HTMLDivElement) => void
newSessionWorktree: string newSessionWorktree: string
onNewSessionWorktreeChange?: (worktree: string) => void
onNewSessionWorktreeReset: () => void onNewSessionWorktreeReset: () => void
onSubmit: () => void onSubmit: () => void
onResponseSubmit: () => void onResponseSubmit: () => void
@@ -265,7 +264,6 @@ export function SessionComposerRegion(props: {
variant={props.placement === "inline" ? "new-session" : undefined} variant={props.placement === "inline" ? "new-session" : undefined}
ref={props.inputRef} ref={props.inputRef}
newSessionWorktree={props.newSessionWorktree} newSessionWorktree={props.newSessionWorktree}
onNewSessionWorktreeChange={props.onNewSessionWorktreeChange}
onNewSessionWorktreeReset={props.onNewSessionWorktreeReset} onNewSessionWorktreeReset={props.onNewSessionWorktreeReset}
edit={props.followup?.edit} edit={props.followup?.edit}
onEditLoaded={props.followup?.onEditLoaded} onEditLoaded={props.followup?.onEditLoaded}
@@ -3,7 +3,7 @@ import { createStore } from "solid-js/store"
import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2" import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
import { useParams } from "@solidjs/router" import { useParams } from "@solidjs/router"
import { showToast } from "@opencode-ai/ui/toast" import { showToast } from "@opencode-ai/ui/toast"
import { useGlobalSync } from "@/context/global-sync" import { useServerSync } from "@/context/server-sync"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { usePermission } from "@/context/permission" import { usePermission } from "@/context/permission"
import { useSDK } from "@/context/sdk" import { useSDK } from "@/context/sdk"
@@ -27,7 +27,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
const params = useParams() const params = useParams()
const sdk = useSDK() const sdk = useSDK()
const sync = useSync() const sync = useSync()
const globalSync = useGlobalSync() const serverSync = useServerSync()
const language = useLanguage() const language = useLanguage()
const permission = usePermission() const permission = usePermission()
@@ -50,7 +50,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
const todos = createMemo((): Todo[] => { const todos = createMemo((): Todo[] => {
const id = params.id const id = params.id
if (!id) return [] if (!id) return []
return globalSync.data.session_todo[id] ?? [] return serverSync.data.session_todo[id] ?? []
}) })
const done = createMemo( const done = createMemo(
@@ -111,7 +111,7 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
const clear = () => { const clear = () => {
const id = params.id const id = params.id
if (!id) return if (!id) return
globalSync.todo.set(id, []) serverSync.todo.set(id, [])
sync.set("todo", id, []) sync.set("todo", id, [])
} }
@@ -23,7 +23,6 @@ export type TimelineRowMap = {
userMessageID: string userMessageID: string
group: PartGroup group: PartGroup
previousAssistantPart: boolean previousAssistantPart: boolean
lastAssistantPart: boolean
} }
Thinking: { userMessageID: string; reasoningHeading?: string } Thinking: { userMessageID: string; reasoningHeading?: string }
Retry: { userMessageID: string } Retry: { userMessageID: string }
@@ -50,7 +49,6 @@ export namespace TimelineRow {
userMessageID: string userMessageID: string
group: PartGroup group: PartGroup
previousAssistantPart: boolean previousAssistantPart: boolean
lastAssistantPart: boolean
}> {} }> {}
export class Thinking extends Data.TaggedClass("Thinking")<{ export class Thinking extends Data.TaggedClass("Thinking")<{
userMessageID: string userMessageID: string
@@ -151,8 +149,6 @@ export namespace Timeline {
), ),
] ]
: groupParts(assistantPartRefs).map((group) => ({ type: "part" as const, group })) : groupParts(assistantPartRefs).map((group) => ({ type: "part" as const, group }))
const assistantGroupCount = assistantItems.filter((item) => item.type === "part").length
if (comments.length > 0) if (comments.length > 0)
rows.push( rows.push(
new TimelineRow.CommentStrip({ new TimelineRow.CommentStrip({
@@ -195,7 +191,6 @@ export namespace Timeline {
userMessageID: userMessage.id, userMessageID: userMessage.id,
group: item.group, group: item.group,
previousAssistantPart: assistantGroupIndex > 0, previousAssistantPart: assistantGroupIndex > 0,
lastAssistantPart: assistantGroupIndex === assistantGroupCount - 1,
}), }),
) )
assistantGroupIndex += 1 assistantGroupIndex += 1
@@ -1,4 +1,16 @@
import { createEffect, createMemo, createSignal, For, Index, on, onCleanup, Show, mapArray, type JSX } from "solid-js" import {
createEffect,
createMemo,
createSignal,
For,
Index,
on,
onCleanup,
Show,
mapArray,
type Accessor,
type JSX,
} from "solid-js"
import { createStore, produce } from "solid-js/store" import { createStore, produce } from "solid-js/store"
import { Dynamic } from "solid-js/web" import { Dynamic } from "solid-js/web"
import { useNavigate } from "@solidjs/router" import { useNavigate } from "@solidjs/router"
@@ -48,11 +60,12 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
import { createResizeObserver } from "@solid-primitives/resize-observer" import { createResizeObserver } from "@solid-primitives/resize-observer"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useSessionKey } from "@/pages/session/session-layout" import { useSessionKey } from "@/pages/session/session-layout"
import { useGlobalSDK } from "@/context/global-sdk" import { useServerSDK } from "@/context/server-sdk"
import { usePlatform } from "@/context/platform" import { usePlatform } from "@/context/platform"
import { useSettings } from "@/context/settings" import { useSettings } from "@/context/settings"
import { useSDK } from "@/context/sdk" import { useSDK } from "@/context/sdk"
import { useSync } from "@/context/sync" import { useSync } from "@/context/sync"
import { notifySessionTabsRemoved } from "@/components/titlebar-session-events"
import { messageAgentColor } from "@/utils/agent" import { messageAgentColor } from "@/utils/agent"
import { sessionTitle } from "@/utils/session-title" import { sessionTitle } from "@/utils/session-title"
import { makeTimer } from "@solid-primitives/timer" import { makeTimer } from "@solid-primitives/timer"
@@ -245,7 +258,7 @@ function TimelineDiffView(props: { diff: SummaryDiff }) {
return ( return (
<div data-slot="session-turn-diff-view" data-scrollable> <div data-slot="session-turn-diff-view" data-scrollable>
<Dynamic component={fileComponent} mode="diff" fileDiff={view.fileDiff} /> <Dynamic component={fileComponent} mode="diff" virtualize={false} fileDiff={view.fileDiff} />
</div> </div>
) )
} }
@@ -273,7 +286,7 @@ export function MessageTimeline(props: {
let touchGesture: number | undefined let touchGesture: number | undefined
const navigate = useNavigate() const navigate = useNavigate()
const globalSDK = useGlobalSDK() const serverSDK = useServerSDK()
const sdk = useSDK() const sdk = useSDK()
const sync = useSync() const sync = useSync()
const settings = useSettings() const settings = useSettings()
@@ -415,8 +428,7 @@ export function MessageTimeline(props: {
if (rows.length === 0) return rows if (rows.length === 0) return rows
return reuseTimelineRows(previous, [...rows, new TimelineRow.BottomSpacer()]) return reuseTimelineRows(previous, [...rows, new TimelineRow.BottomSpacer()])
}) })
const timelineRowByKey = createMemo(() => new Map(timelineRows().map((row) => [TimelineRow.key(row), row] as const))) const timelineRowKeys = createMemo(() => timelineRows().map(TimelineRow.key), [] as string[], { equals: sameKeys })
const timelineRowKeys = createMemo(() => [...timelineRowByKey().keys()], [] as string[], { equals: sameKeys })
const virtualCache = createMemo(() => readTimelineCache(sessionKey(), timelineRowKeys())) const virtualCache = createMemo(() => readTimelineCache(sessionKey(), timelineRowKeys()))
const messageRowIndex = createMemo(() => { const messageRowIndex = createMemo(() => {
const result = new Map<string, number>() const result = new Map<string, number>()
@@ -427,6 +439,14 @@ export function MessageTimeline(props: {
}) })
return result return result
}) })
const lastAssistantGroupKey = createMemo(() => {
const result = new Map<string, string>()
timelineRows().forEach((row) => {
if (row._tag !== "AssistantPart") return
result.set(row.userMessageID, row.group.key)
})
return result
})
const keepMounted = createMemo(() => { const keepMounted = createMemo(() => {
const id = activeMessageID() const id = activeMessageID()
if (!id) return if (!id) return
@@ -540,6 +560,7 @@ export function MessageTimeline(props: {
const [bar, setBar] = createStore({ const [bar, setBar] = createStore({
ms: pace(640), ms: pace(640),
}) })
const [toolOpen, setToolOpen] = createStore<Record<string, boolean | undefined>>({})
let more: HTMLButtonElement | undefined let more: HTMLButtonElement | undefined
let head: HTMLDivElement | undefined let head: HTMLDivElement | undefined
@@ -560,6 +581,11 @@ export function MessageTimeline(props: {
const isMeasuredBottom = (root: HTMLDivElement) => root.scrollHeight - root.clientHeight - root.scrollTop <= 4 const isMeasuredBottom = (root: HTMLDivElement) => root.scrollHeight - root.clientHeight - root.scrollTop <= 4
const measureTimeline = () => {
virtualizer?.measure()
anchorMeasuredBottom()
}
function anchorMeasuredBottom() { function anchorMeasuredBottom() {
if (!listRoot) return false if (!listRoot) return false
if (!measuredBottomAnchored) return false if (!measuredBottomAnchored) return false
@@ -704,14 +730,14 @@ export function MessageTimeline(props: {
} }
const shareMutation = useMutation(() => ({ const shareMutation = useMutation(() => ({
mutationFn: (id: string) => globalSDK.client.session.share({ sessionID: id, directory: sdk.directory }), mutationFn: (id: string) => serverSDK.client.session.share({ sessionID: id, directory: sdk.directory }),
onError: (err) => { onError: (err) => {
console.error("Failed to share session", err) console.error("Failed to share session", err)
}, },
})) }))
const unshareMutation = useMutation(() => ({ const unshareMutation = useMutation(() => ({
mutationFn: (id: string) => globalSDK.client.session.unshare({ sessionID: id, directory: sdk.directory }), mutationFn: (id: string) => serverSDK.client.session.unshare({ sessionID: id, directory: sdk.directory }),
onError: (err) => { onError: (err) => {
console.error("Failed to unshare session", err) console.error("Failed to unshare session", err)
}, },
@@ -836,7 +862,9 @@ export function MessageTimeline(props: {
if (index !== -1) draft.session.splice(index, 1) if (index !== -1) draft.session.splice(index, 1)
}), }),
) )
sync.session.evict(sessionID)
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id) navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
notifySessionTabsRemoved({ directory: sdk.directory, sessionIDs: [sessionID] })
}) })
.catch((err) => { .catch((err) => {
showToast({ showToast({
@@ -867,42 +895,46 @@ export function MessageTimeline(props: {
if (!result) return false if (!result) return false
const removed = new Set<string>([sessionID])
const byParent = new Map<string, string[]>()
for (const item of sync.data.session) {
const parentID = item.parentID
if (!parentID) continue
const existing = byParent.get(parentID)
if (existing) {
existing.push(item.id)
continue
}
byParent.set(parentID, [item.id])
}
const stack = [sessionID]
while (stack.length) {
const parentID = stack.pop()
if (!parentID) continue
const children = byParent.get(parentID)
if (!children) continue
for (const child of children) {
if (removed.has(child)) continue
removed.add(child)
stack.push(child)
}
}
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
sync.set( sync.set(
produce((draft) => { produce((draft) => {
const removed = new Set<string>([sessionID])
const byParent = new Map<string, string[]>()
for (const item of draft.session) {
const parentID = item.parentID
if (!parentID) continue
const existing = byParent.get(parentID)
if (existing) {
existing.push(item.id)
continue
}
byParent.set(parentID, [item.id])
}
const stack = [sessionID]
while (stack.length) {
const parentID = stack.pop()
if (!parentID) continue
const children = byParent.get(parentID)
if (!children) continue
for (const child of children) {
if (removed.has(child)) continue
removed.add(child)
stack.push(child)
}
}
draft.session = draft.session.filter((s) => !removed.has(s.id)) draft.session = draft.session.filter((s) => !removed.has(s.id))
}), }),
) )
navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id) for (const id of removed) {
sync.session.evict(id)
}
notifySessionTabsRemoved({ directory: sdk.directory, sessionIDs: [...removed] })
return true return true
} }
@@ -980,29 +1012,42 @@ export function MessageTimeline(props: {
const getMsgPart = (messageID: string, partID: string) => getMsgParts(messageID).find((part) => part.id === partID) const getMsgPart = (messageID: string, partID: string) => getMsgParts(messageID).find((part) => part.id === partID)
const renderAssistantPartGroup = (row: TimelineRowMap["AssistantPart"]) => { const renderAssistantPartGroup = (row: Accessor<TimelineRowMap["AssistantPart"]>) => {
if (row.group.type === "context") { if (row().group.type === "context") {
const parts = createMemo(() => { const parts = createMemo(() => {
const group = row.group const group = row().group
if (group.type !== "context") return emptyTools if (group.type !== "context") return emptyTools
return group.refs return group.refs
.map((ref) => getMsgPart(ref.messageID, ref.partID)) .map((ref) => getMsgPart(ref.messageID, ref.partID))
.filter((part): part is ToolPart => part?.type === "tool") .filter((part): part is ToolPart => part?.type === "tool")
}) })
return <ContextToolGroup parts={parts()} busy={workingTurn(row.userMessageID) && row.lastAssistantPart} /> return (
<ContextToolGroup
parts={parts()}
busy={
workingTurn(row().userMessageID) && lastAssistantGroupKey().get(row().userMessageID) === row().group.key
}
onSizeChange={measureTimeline}
/>
)
} }
const message = createMemo(() => { const message = createMemo(() => {
const group = row.group const group = row().group
if (group.type !== "part") return if (group.type !== "part") return
return messageByID().get(group.ref.messageID) return messageByID().get(group.ref.messageID)
}) })
const part = createMemo(() => { const part = createMemo(() => {
const group = row.group const group = row().group
if (group.type !== "part") return if (group.type !== "part") return
return getMsgPart(group.ref.messageID, group.ref.partID) return getMsgPart(group.ref.messageID, group.ref.partID)
}) })
const defaultOpen = createMemo(() => {
const item = part()
if (!item) return
return partDefaultOpen(item, settings.general.shellToolPartsExpanded(), settings.general.editToolPartsExpanded())
})
return ( return (
<Show when={message()}> <Show when={message()}>
@@ -1012,14 +1057,13 @@ export function MessageTimeline(props: {
<MessagePart <MessagePart
part={part()} part={part()}
message={message()} message={message()}
showAssistantCopyPartID={assistantCopyPartID(row.userMessageID)} showAssistantCopyPartID={assistantCopyPartID(row().userMessageID)}
turnDurationMs={turnDurationMs(row.userMessageID)} turnDurationMs={turnDurationMs(row().userMessageID)}
defaultOpen={partDefaultOpen( defaultOpen={defaultOpen()}
part(), toolOpen={toolOpen[part().id] ?? defaultOpen()}
settings.general.shellToolPartsExpanded(), onToolOpenChange={(open) => setToolOpen(part().id, open)}
settings.general.editToolPartsExpanded(),
)}
deferToolContent={false} deferToolContent={false}
virtualizeDiff={false}
/> />
)} )}
</Show> </Show>
@@ -1028,25 +1072,25 @@ export function MessageTimeline(props: {
) )
} }
function TimelineRowFrame(input: { row: FramedTimelineRow; children: JSX.Element }) { function TimelineRowFrame(input: { row: Accessor<FramedTimelineRow>; children: JSX.Element }) {
const anchor = () => { const anchor = () => {
const row = input.row const row = input.row()
return row._tag === "CommentStrip" || (row._tag === "UserMessage" && row.anchor) return row._tag === "CommentStrip" || (row._tag === "UserMessage" && row.anchor)
} }
const previousUserMessage = () => { const previousUserMessage = () => {
const row = input.row const row = input.row()
return (row._tag === "CommentStrip" || row._tag === "UserMessage") && row.previousUserMessage return (row._tag === "CommentStrip" || row._tag === "UserMessage") && row.previousUserMessage
} }
const previousAssistantPart = () => { const previousAssistantPart = () => {
const row = input.row const row = input.row()
return row._tag === "AssistantPart" && row.previousAssistantPart return row._tag === "AssistantPart" && row.previousAssistantPart
} }
return ( return (
<div <div
id={anchor() ? props.anchor(input.row.userMessageID) : undefined} id={anchor() ? props.anchor(input.row().userMessageID) : undefined}
data-message-id={input.row.userMessageID} data-message-id={input.row().userMessageID}
data-timeline-row={input.row._tag} data-timeline-row={input.row()._tag}
classList={{ classList={{
"min-w-0 w-full max-w-full": true, "min-w-0 w-full max-w-full": true,
"md:max-w-200 2xl:max-w-[1000px]": props.centered, "md:max-w-200 2xl:max-w-[1000px]": props.centered,
@@ -1062,14 +1106,15 @@ export function MessageTimeline(props: {
) )
} }
const renderTimelineRow = (row: TimelineRow.TimelineRow) => { const renderTimelineRow = (row: Accessor<TimelineRow.TimelineRow>) => {
switch (row._tag) { switch (row()._tag) {
case "CommentStrip": { case "CommentStrip": {
const commentStripRow = row as Accessor<TimelineRowByTag<"CommentStrip">>
const comments = createMemo(() => const comments = createMemo(() =>
getMsgParts(row.userMessageID).flatMap((part) => MessageComment.fromPart(part) ?? []), getMsgParts(commentStripRow().userMessageID).flatMap((part) => MessageComment.fromPart(part) ?? []),
) )
return ( return (
<TimelineRowFrame row={row}> <TimelineRowFrame row={commentStripRow}>
<div class="w-full px-4 md:px-5 pb-2"> <div class="w-full px-4 md:px-5 pb-2">
<div class="ml-auto max-w-[82%] overflow-x-auto no-scrollbar"> <div class="ml-auto max-w-[82%] overflow-x-auto no-scrollbar">
<div class="flex w-max min-w-full justify-end gap-2"> <div class="flex w-max min-w-full justify-end gap-2">
@@ -1102,17 +1147,22 @@ export function MessageTimeline(props: {
) )
} }
case "UserMessage": { case "UserMessage": {
const userMessageRow = row as Accessor<TimelineRowByTag<"UserMessage">>
const message = createMemo(() => { const message = createMemo(() => {
const m = messageByID().get(row.userMessageID) const m = messageByID().get(userMessageRow().userMessageID)
if (m?.role === "user") return m if (m?.role === "user") return m
}) })
return ( return (
<TimelineRowFrame row={row}> <TimelineRowFrame row={userMessageRow}>
<Show when={message()}> <Show when={message()}>
{(message) => ( {(message) => (
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5"> <div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
<div data-slot="session-turn-message-content" aria-live="off"> <div data-slot="session-turn-message-content" aria-live="off">
<Message message={message()} parts={getMsgParts(row.userMessageID)} actions={props.actions} /> <Message
message={message()}
parts={getMsgParts(userMessageRow().userMessageID)}
actions={props.actions}
/>
</div> </div>
</div> </div>
)} )}
@@ -1121,13 +1171,14 @@ export function MessageTimeline(props: {
) )
} }
case "TurnDivider": { case "TurnDivider": {
const turnDividerRow = row as Accessor<TimelineRowByTag<"TurnDivider">>
return ( return (
<TimelineRowFrame row={row}> <TimelineRowFrame row={turnDividerRow}>
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5"> <div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
<div data-slot="session-turn-compaction"> <div data-slot="session-turn-compaction">
<MessageDivider <MessageDivider
label={language.t( label={language.t(
row.label === "compaction" ? "ui.messagePart.compaction" : "ui.message.interrupted", turnDividerRow().label === "compaction" ? "ui.messagePart.compaction" : "ui.message.interrupted",
)} )}
/> />
</div> </div>
@@ -1136,22 +1187,27 @@ export function MessageTimeline(props: {
) )
} }
case "AssistantPart": { case "AssistantPart": {
const assistantPartRow = row as Accessor<TimelineRowByTag<"AssistantPart">>
return ( return (
<TimelineRowFrame row={row}> <TimelineRowFrame row={assistantPartRow}>
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5"> <div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
<div data-slot="session-turn-assistant-content" aria-hidden={workingTurn(row.userMessageID)}> <div
{renderAssistantPartGroup(row)} data-slot="session-turn-assistant-content"
aria-hidden={workingTurn(assistantPartRow().userMessageID)}
>
{renderAssistantPartGroup(assistantPartRow)}
</div> </div>
</div> </div>
</TimelineRowFrame> </TimelineRowFrame>
) )
} }
case "Thinking": { case "Thinking": {
const thinkingRow = row as Accessor<TimelineRowByTag<"Thinking">>
return ( return (
<TimelineRowFrame row={row}> <TimelineRowFrame row={thinkingRow}>
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5"> <div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
<TimelineThinkingRow <TimelineThinkingRow
reasoningHeading={row.reasoningHeading} reasoningHeading={thinkingRow().reasoningHeading}
showReasoningSummaries={settings.general.showReasoningSummaries()} showReasoningSummaries={settings.general.showReasoningSummaries()}
/> />
</div> </div>
@@ -1159,29 +1215,32 @@ export function MessageTimeline(props: {
) )
} }
case "Retry": { case "Retry": {
const retryRow = row as Accessor<TimelineRowByTag<"Retry">>
return ( return (
<TimelineRowFrame row={row}> <TimelineRowFrame row={retryRow}>
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5"> <div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
<SessionRetry status={sessionStatus()} show={activeMessageID() === row.userMessageID} /> <SessionRetry status={sessionStatus()} show={activeMessageID() === retryRow().userMessageID} />
</div> </div>
</TimelineRowFrame> </TimelineRowFrame>
) )
} }
case "DiffSummary": { case "DiffSummary": {
const diffSummaryRow = row as Accessor<TimelineRowByTag<"DiffSummary">>
return ( return (
<TimelineRowFrame row={row}> <TimelineRowFrame row={diffSummaryRow}>
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5"> <div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
<TimelineDiffSummaryRow diffs={row.diffs} /> <TimelineDiffSummaryRow diffs={diffSummaryRow().diffs} />
</div> </div>
</TimelineRowFrame> </TimelineRowFrame>
) )
} }
case "Error": { case "Error": {
const errorRow = row as Accessor<TimelineRowByTag<"Error">>
return ( return (
<TimelineRowFrame row={row}> <TimelineRowFrame row={errorRow}>
<div data-slot="session-turn-message-container" class="w-full px-4 md:px-5"> <div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
<Card variant="error" class="error-card"> <Card variant="error" class="error-card">
{row.text} {errorRow().text}
</Card> </Card>
</div> </div>
</TimelineRowFrame> </TimelineRowFrame>
@@ -1192,12 +1251,8 @@ export function MessageTimeline(props: {
} }
} }
function TimelineRowView(props: { rowKey: string }) { function TimelineRowView(props: { row: TimelineRow.TimelineRow }) {
return ( return renderTimelineRow(() => props.row)
<Show when={timelineRowByKey().get(props.rowKey)} keyed>
{(item) => renderTimelineRow(item)}
</Show>
)
} }
return ( return (
@@ -1529,7 +1584,7 @@ export function MessageTimeline(props: {
<Show when={scrollRoot()}> <Show when={scrollRoot()}>
{(root) => ( {(root) => (
<Virtualizer <Virtualizer
data={timelineRowKeys()} data={timelineRows()}
cache={virtualCache()} cache={virtualCache()}
itemSize={virtualCache() ? undefined : timelineFallbackItemSize} itemSize={virtualCache() ? undefined : timelineFallbackItemSize}
scrollRef={root()} scrollRef={root()}
@@ -1549,7 +1604,7 @@ export function MessageTimeline(props: {
scheduleContentRoot(root()) scheduleContentRoot(root())
}} }}
> >
{(key) => <TimelineRowView rowKey={key} />} {(row) => <TimelineRowView row={row} />}
</Virtualizer> </Virtualizer>
)} )}
</Show> </Show>
@@ -0,0 +1,14 @@
import { describe, expect, test } from "bun:test"
import { shouldUseV2NewSessionPage } from "./new-session-layout"
describe("shouldUseV2NewSessionPage", () => {
test("keeps disabled pages on the legacy layout", () => {
expect(shouldUseV2NewSessionPage({ newLayoutDesigns: false, sessionID: "ses_123" })).toBe(false)
expect(shouldUseV2NewSessionPage({ newLayoutDesigns: false })).toBe(false)
})
test("uses the v2 layout only for enabled new-session pages", () => {
expect(shouldUseV2NewSessionPage({ newLayoutDesigns: true })).toBe(true)
expect(shouldUseV2NewSessionPage({ newLayoutDesigns: true, sessionID: "ses_123" })).toBe(false)
})
})
@@ -0,0 +1,3 @@
export function shouldUseV2NewSessionPage(input: { newLayoutDesigns: boolean; sessionID?: string }) {
return input.newLayoutDesigns && !input.sessionID
}
@@ -55,15 +55,11 @@ export function SessionSidePanel(props: {
const language = useLanguage() const language = useLanguage()
const command = useCommand() const command = useCommand()
const dialog = useDialog() const dialog = useDialog()
const { sessionKey, tabs, view } = useSessionLayout() const { sessionKey, tabs, view, params } = useSessionLayout()
const isDesktop = createMediaQuery("(min-width: 768px)") const isDesktop = createMediaQuery("(min-width: 768px)")
const shown = createMemo( const desktopV2 = () => platform.platform === "desktop" && settings.general.newLayoutDesigns()
() => const shown = createMemo(() => (desktopV2() ? settings.general.showFileTree() : true))
platform.platform !== "desktop" ||
import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" ||
settings.general.showFileTree(),
)
const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened())
const fileOpen = createMemo(() => isDesktop() && shown() && layout.fileTree.opened()) const fileOpen = createMemo(() => isDesktop() && shown() && layout.fileTree.opened())
@@ -207,7 +203,7 @@ export function SessionSidePanel(props: {
}) })
return ( return (
<Show when={isDesktop()}> <Show when={isDesktop() && !(settings.general.newLayoutDesigns() && !params.id)}>
<aside <aside
id="review-panel" id="review-panel"
aria-label={language.t("session.panel.reviewAndFiles")} aria-label={language.t("session.panel.reviewAndFiles")}
@@ -70,10 +70,8 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
}) })
const activeFileTab = tabState.activeFileTab const activeFileTab = tabState.activeFileTab
const closableTab = tabState.closableTab const closableTab = tabState.closableTab
const shown = () => const desktopV2 = () => platform.platform === "desktop" && settings.general.newLayoutDesigns()
platform.platform !== "desktop" || const shown = () => (desktopV2() ? settings.general.showFileTree() : true)
import.meta.env.VITE_OPENCODE_CHANNEL !== "beta" ||
settings.general.showFileTree()
const messages = () => { const messages = () => {
const id = params.id const id = params.id

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