Compare commits

...
Author SHA1 Message Date
Brendan Allan 1140d8422e chore(app): schedule layout sunset outside prod 2026-07-14 14:36:34 +00:00
Brendan Allan 9ecfb847fa fix(desktop): derive layout eligibility from install state 2026-07-14 14:06:26 +00:00
usrnk1 588288ce87 test(app): add layout transition stories 2026-07-13 21:36:41 +02:00
usrnk1 0156d3028f feat(app): simplify layout transition messaging 2026-07-13 21:36:08 +02:00
usrnk1 5e5e305bb7 fix(app): avoid layout flash during cohort detection 2026-07-13 14:40:32 +02:00
usrnk1 d87f987800 feat(app): preserve previous layout for active users 2026-07-13 14:37:01 +02:00
usrnk1 545b86587d feat(app): detect prior server activity for layout transition 2026-07-13 14:36:34 +02:00
usrnk1 7fb3fbdf27 feat(app): clarify layout transition timing 2026-07-13 13:35:26 +02:00
usrnk1 31f946eed7 feat(app): enforce layout sunset for existing users 2026-07-13 13:35:08 +02:00
usrnk1 45daa7fece feat(app): localize interface retirement notice 2026-07-13 13:08:13 +02:00
usrnk1 a5bbe636e7 feat(app): clarify interface transition and retirement 2026-07-13 13:06:17 +02:00
usrnk1 2e1ce87bf3 feat(app): add interface transition setting 2026-07-13 13:05:46 +02:00
usrnk1 56e8a4f7d7 feat(ui): add accent badge variant 2026-07-13 13:05:46 +02:00
b3a012cbdd fix(cli): switch org after console logout (#36276)
Co-authored-by: Victor Navarro <vn4varro@gmail.com>
2026-07-13 10:59:16 +00:00
usrnk1andGitHub 51b9c726cf fix(app): remove interface transition changes accidentally merged into dev (#36653) 2026-07-13 10:38:30 +00:00
b4e49d5b32 feat(app): redesign attachment cards (#35945)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
2026-07-13 10:04:45 +00:00
opencode-agent[bot] 2b29854589 chore: generate 2026-07-13 10:04:14 +00:00
usrnk1 8ce5038bd8 Merge branch 'dev' of https://github.com/anomalyco/opencode into interface-toggle 2026-07-13 12:02:25 +02:00
usrnk1 446510a6a9 tui: surface a dismissible new-interface notice and tighten settings copy for clarity
Users who were migrated to the new interface will now see a dismissible notice in Settings explaining the old interface was phased out, so they are not surprised by the change. The new-interface toggle is also visually elevated with a card style so it is easier to find. Settings labels and descriptions across General have been rewritten to be shorter and more direct, so it is quicker to understand what each option controls. The "New" badge color is also adjusted for better readability.
2026-07-13 12:01:19 +02:00
opencode-agent[bot] 49028b6245 chore: generate 2026-07-13 09:33:54 +00:00
d595c7e265 feat(app): review panel updates (#36240)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-07-13 09:32:25 +00:00
17cd4a8c32 feat(app): align edit project modal with v2 style (#36213)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-07-13 09:26:53 +00:00
opencode-agent[bot] 9bbf7918de chore: generate 2026-07-13 09:25:14 +00:00
Aarav SareenandGitHub 97f502e79a feat(app): middle click to open new tab (#36215) 2026-07-13 17:23:55 +08:00
usrnk1 ac5e249083 feat(app): add interface transition setting 2026-07-13 10:45:57 +02:00
usrnk1 140e796b21 feat(ui): add accent badge variant 2026-07-13 10:45:57 +02:00
dd02cea9e7 feat(desktop): fix clipped labels and branch tooltip (#35724)
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
2026-07-13 08:02:38 +00:00
Aiden ClineandGitHub a8062ea314 fix(provider): derive variants from reasoning metadata (#36624) 2026-07-13 01:21:59 -05:00
Aiden ClineandGitHub f47684787a fix(opencode): filter unsupported GPT-5.6 OAuth alias (#36621) 2026-07-13 00:23:28 -05:00
Aiden ClineandGitHub 8168f0f0f6 fix(provider): route gateway variants by api id (#36614) 2026-07-13 00:08:08 -05:00
8036440693 ci: remove starptech from core triage assignees (#36618)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-07-12 23:41:02 -05:00
opencode-agent[bot] cf7503687a chore: generate 2026-07-12 21:13:21 +00:00
Aiden ClineandGitHub 6f8e1dda15 fix(provider): derive variants from reasoning options (#36543) 2026-07-12 16:12:07 -05:00
opencode-agent[bot] 4dcfd9182c chore: generate 2026-07-12 19:42:58 +00:00
d7c0db8cee fix(openai): use codex context limits for gpt-5.6 (#36248)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-07-12 14:41:51 -05:00
Aiden ClineandGitHub 184da0e42e test(opencode): refresh stale model references (#36546) 2026-07-12 13:28:36 -05:00
Aiden ClineandGitHub a244d82aba test(opencode): refresh models.dev fixture (#36541) 2026-07-12 12:36:20 -05:00
opencode-agent[bot]GitHubDaxopencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
34e5809059 feat(tui): show idle session directory (#36457)
Co-authored-by: Dax <mail@thdxr.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-07-11 20:31:57 +00:00
2db96c9b7e fix(core): disable unused fff content caches (#36453)
Co-authored-by: Dax Raad <d@ironbay.co>
2026-07-11 19:12:58 +00:00
Frank 9976269ab1 docs: publish GPT 5.6 Sol, Terra, and Luna model availability and pricing across all language docs 2026-07-10 14:13:46 -04:00
Adam 5df822be11 fix(stats): polish comparison details 2026-07-10 12:42:04 -05:00
Adam 233f069070 feat(stats): add comparison seo pages 2026-07-10 12:09:56 -05:00
Adam 4a1982f5c9 feat(stats): add model comparison radar 2026-07-10 11:41:01 -05:00
Adam 1c7f65f105 fix(stats): stop comparison overscroll 2026-07-10 07:31:00 -05:00
Adam a84151e482 fix(stats): restore sticky comparison header 2026-07-10 07:30:00 -05:00
Adam 0340a4ff77 fix(stats): restore native trackpad scrolling 2026-07-10 07:09:25 -05:00
Adam f4faacbe87 feat(stats): unify comparison cards 2026-07-10 06:49:56 -05:00
opencode-agent[bot] 7dbfd0b934 chore: generate 2026-07-10 11:38:07 +00:00
Adam 08a2a623dc fix(data): cut athena stats sync cost
Compute all stat dimensions and both grains in one GROUPING SETS query
so each sync pass scans the source table once instead of eight times.
Hourly passes now only recompute the current ISO week; a daily full pass
refreshes the whole display window. Restarted daemons resume the hourly
cadence from the last completed sync instead of immediately re-running a
pass, and the workgroup kills any query scanning more than 2 TB.
2026-07-10 06:36:26 -05:00
Adam 60c88336b9 feat(stats): update model comparison page 2026-07-10 05:53:31 -05:00
Brendan AllanandGitHub 8a03fc265b feat(app): extract prompt state and add composer model selection (#36227) 2026-07-10 15:49:05 +08:00
Frank d0ba538924 zen: track openai cache_write_tokens 2026-07-10 01:48:51 -04:00
Aarav SareenandGitHub 772e976d80 fix(app): timeline width jumps around when review panel is open/closed (#36202) 2026-07-10 03:59:03 +00:00
Aarav SareenandGitHub c769380776 feat(app): visual improvements (#36073) 2026-07-10 10:52:05 +08:00
01131c75d8 fix(app): focus input on new session (#36077)
Co-authored-by: Victor Navarro <vn4varro@gmail.com>
2026-07-10 10:41:54 +08:00
Brendan AllanandGitHub ea982c3832 fix(app): wrap session creation state updates in startTransition (#36182) 2026-07-10 02:29:30 +00:00
opencode-agent[bot] d09d5612f3 chore: generate 2026-07-10 00:55:59 +00:00
Adam 85ef35dd64 fix(data): oom crash in stats sync 2026-07-09 19:53:56 -05:00
251177d56c fix(tui): forward environment to worker (#35925)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-07-09 18:30:03 -05:00
13b9e55c72 fix(ui): sometimes outlines get clipped (#33846)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-07-10 09:13:34 +10:00
Luke ParkerandGitHub b94e55c7c0 fix(app): align context tokens with usage (#36015) 2026-07-10 09:12:54 +10:00
Luke ParkerandGitHub 9c6bb4ff72 fix(app): show tree while opening files (#36018) 2026-07-10 09:12:20 +10:00
opencode c2e019ba7d sync release versions for v1.17.18 2026-07-09 18:51:42 +00:00
Aiden ClineandGitHub b8374b5a7c fix(copilot): handle zero billing batch size (#36123) 2026-07-09 13:14:33 -05:00
Aiden ClineandGitHub b4665a8bf8 feat: add meta muse system prompt (#36122) 2026-07-09 13:00:39 -05:00
opencode 08096b5e61 sync release versions for v1.17.17 2026-07-09 15:03:10 +00:00
Aiden ClineandGitHub 2e43d41af0 tweak: adjust variant logic for muse (#36079) 2026-07-09 09:07:02 -05:00
Frank 518772c2ba Merge branch 'dev' of github.com:anomalyco/opencode into dev 2026-07-09 08:03:18 -04:00
Frank fc9021ccec zen: retry on 529 2026-07-09 08:03:16 -04:00
opencode-agent[bot] 3bebfe6b1b chore: update nix node_modules hashes 2026-07-09 11:41:13 +00:00
opencode-agent[bot] 3b18c64782 chore: generate 2026-07-09 11:26:18 +00:00
c6c599b872 feat(app): refactor help button and add tabs info popup persistence (#36066)
Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com>
2026-07-09 11:24:39 +00:00
opencode-agent[bot] 6b41ae910c chore: generate 2026-07-09 08:08:24 +00:00
usrnk1andGitHub 5c860d4142 feat(feat): align sub-agent task rows with v2 design (#35167) 2026-07-09 16:07:02 +08:00
usrnk1andGitHub 4f9207daac feat(app): restyle revert dock for v2 (#35560) 2026-07-09 16:06:00 +08:00
Aarav SareenandGitHub ae7d63272c feat(app): fix descender clipping (#35950) 2026-07-09 15:09:04 +08:00
5cc3a51357 feat(app): free model selector (#35707)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-07-09 07:05:09 +00:00
opencode e357c90212 sync release versions for v1.17.16 2026-07-09 06:35:57 +00:00
opencode-agent[bot] fb1edc4918 chore: update nix node_modules hashes 2026-07-09 05:48:48 +00:00
opencode-agent[bot] 353e0eef8a chore: generate 2026-07-09 05:43:07 +00:00
usrnk1andGitHub a4c17f08b1 feat(desktop): reveal projects in file manager (#35731) 2026-07-09 13:41:56 +08:00
usrnk1andGitHub 3cd9ee5a73 feat(app): add composer add menu with draft-preserving commands (#35711) 2026-07-09 13:39:55 +08:00
opencode-agent[bot] 7c56319aa4 chore: generate 2026-07-09 05:28:34 +00:00
13fa4937b2 feat(desktop): add provider connection tip (#35948)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-07-09 13:27:22 +08:00
Aiden ClineandGitHub 1db5c2402c fix(opencode): expose Grok reasoning variants (#36017) 2026-07-09 00:13:10 -05:00
0abbcddac2 chore: add missing team members (#35980)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-07-09 09:53:05 +10:00
Luke ParkerandGitHub dd25d143c5 fix(app): reserve review pane minimum instead of capping chat width at 45% (#35078) 2026-07-09 09:47:46 +10:00
Luke ParkerandGitHub d8a57ee15c fix(app): unmount hidden session panes (#35628) 2026-07-09 09:44:02 +10:00
Luke ParkerandGitHub aa52d30d7f feat(app): persist review state per session (#35488) 2026-07-08 23:43:28 +00:00
Luke ParkerandGitHub 83d633fd13 fix(app): route legacy drafts to session page (#35808) 2026-07-09 07:38:56 +08:00
Luke ParkerandGitHub 824455e2f1 fix(app): load capped review patches (#35633) 2026-07-09 09:09:32 +10:00
Luke ParkerandGitHub 62de2b3695 feat(app): add inline file browser tabs (#35829) 2026-07-09 09:09:21 +10:00
opencode-agent[bot] 7550aa4b89 chore: update nix node_modules hashes 2026-07-08 22:51:45 +00:00
opencode-agent[bot]andGitHub c33a2011c4 fix(app): prevent command palette first-open flash (#35858) 2026-07-09 06:51:04 +08:00
David HillandGitHub fff6764ed4 fix(console): update square avatar previews (#35975) 2026-07-08 22:48:11 +00:00
opencode-agent[bot] 9644a008a4 chore: generate 2026-07-08 22:31:09 +00:00
Aiden ClineandGitHub ccb6b7c3ea fix: improve xai cache hit rate (#35970) 2026-07-08 17:29:36 -05:00
f95f877e5f fix(core): watch only git projects (#35957)
Co-authored-by: Kit Langton <kit.langton@gmail.com>
2026-07-08 20:58:41 +00:00
Frank fe96d4aebd zen: grok 4.5 2026-07-08 15:52:56 -04:00
Frank 3a95d56144 Merge branch 'dev' of https://github.com/anomalyco/opencode into dev 2026-07-08 15:30:52 -04:00
Frank ecd8be7593 zen: grok 4.5 2026-07-08 15:30:49 -04:00
opencode-agent[bot] a72c3880bb chore: generate 2026-07-08 16:30:17 +00:00
Adam ccd8676bee feat(stats): add model comparison home 2026-07-08 11:28:28 -05:00
Adam 5a533e366f feat(stats): add model comparison pages 2026-07-08 11:28:27 -05:00
Luke ParkerandGitHub 77429f5982 feat(app): sync product translations (#35856) 2026-07-08 14:57:28 +10:00
Brendan AllanandGitHub 6cfe4bc34f fix(app): route new session shortcut through tabs (#35849) 2026-07-08 11:44:56 +08:00
Brendan AllanandGitHub 95013d2936 fix(app): keep session routes within layouts (#35842) 2026-07-08 03:24:21 +00:00
Adam 14a5529793 feat(data): redesign model peers 2026-07-07 11:15:43 -05:00
opencode 0b2e8707e3 sync release versions for v1.17.15 2026-07-07 15:31:36 +00:00
冯基魁andGitHub adf178a6b9 fix(llm): classify zai token limit overflow (#35671) 2026-07-07 09:46:27 -05:00
78f85b1cd6 fix(core): ensure relevant files survive compaction (#35636)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-07-07 09:39:01 -05:00
opencode-agent[bot] 888dc67cc3 chore: generate 2026-07-07 14:25:21 +00:00
Dustin DeusandGitHub fd9ee435ec fix(core): expand home-relative permission paths (#35737) 2026-07-07 16:23:50 +02:00
opencode-agent[bot] 1c25b2f298 chore: generate 2026-07-07 11:55:25 +00:00
Adam ea9fffce2e feat(data): redesign model efficiency 2026-07-07 06:53:36 -05:00
Brendan AllanandGitHub 516f0266b8 fix(app): hide markdown task markers (#35670) 2026-07-07 15:22:49 +08:00
opencode-agent[bot] 39397deb8d chore: generate 2026-07-07 06:44:40 +00:00
Brendan AllanandGitHub 65fd2e5c91 fix(app): prioritize shortcuts in terminal (#35668) 2026-07-07 06:43:30 +00:00
opencode-agent[bot]GitHubLuke Parkeropencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>Brendan Allan
c5fe32fbb1 fix(app): show unread for pending questions (#34684)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-07-07 13:27:19 +08:00
opencode-agent[bot] c48b77d6f9 chore: generate 2026-07-07 05:12:15 +00:00
Brendan AllanandGitHub 64fd7a2027 fix(app): persist session tab info (#34934) 2026-07-07 05:11:08 +00:00
ae259d87f0 fix(app): apply settings themes on select (#35011)
Co-authored-by: Jay <53023+jayair@users.noreply.github.com>
2026-07-07 13:03:25 +08:00
Aarav SareenandGitHub 50374abab8 feat(app): change traffic light position, fix sequoia bug (#35081) 2026-07-07 12:49:28 +08:00
opencode-agent[bot] 0497e8badf chore: generate 2026-07-07 04:46:52 +00:00
Aarav SareenandGitHub 2a08f8f5f4 feat(app): reintroduce model context tooltip in model selector (#35087) 2026-07-07 12:45:42 +08:00
opencode-agent[bot] 9353559088 chore: generate 2026-07-07 02:37:53 +00:00
Aarav SareenandGitHub 6aff2ee169 feat(app): visual improvements (#35134) 2026-07-07 10:36:27 +08:00
254a481e5d fix(config): handle unavailable config directories (#35632)
Co-authored-by: James Long <jlongster@users.noreply.github.com>
2026-07-06 22:11:13 -04:00
26885e7118 feat(app): align cmd k menu with v2 styles (#35152)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-07-07 08:52:57 +08:00
opencode-agent[bot] dba08013d1 chore: generate 2026-07-06 20:10:55 +00:00
Adam fc09646973 fix(data): refine model trend charts 2026-07-06 15:03:07 -05:00
Adam c3b9c9d888 feat(data): improved model usage section 2026-07-06 15:03:07 -05:00
opencode 203885d241 sync release versions for v1.17.14 2026-07-06 18:50:51 +00:00
opencode-agent[bot] 728e86b287 chore: generate 2026-07-06 18:20:49 +00:00
Aiden ClineandGitHub d5aa79c73a feat(codemode): sync v2 implementation (#35574) 2026-07-06 13:19:21 -05:00
opencode-agent[bot] eb6ff0c1e0 chore: update nix node_modules hashes 2026-07-06 13:35:10 +00:00
opencode-agent[bot] 0df44c4e91 chore: generate 2026-07-06 11:02:19 +00:00
Simon KleeandGitHub 561070fbc2 deps: upgrade OpenTUI to v0.4.3 (#35226) 2026-07-06 13:01:08 +02:00
Brendan AllanandGitHub b0e41ff2c4 fix(app): use selected home project for new sessions (#35530) 2026-07-06 16:37:50 +08:00
opencode-agent[bot] 977a40af68 chore: generate 2026-07-06 08:21:25 +00:00
Brendan AllanandGitHub dffecb6478 fix(desktop): gate first launch onboarding (#34930) 2026-07-06 16:20:15 +08:00
Brendan AllanandGitHub 7f57d2a9ac feat(app): show draft server status in titlebar (#35521) 2026-07-06 16:12:53 +08:00
377d5d2287 fix(app): avoid shortcut settings flash (#35349)
Co-authored-by: Jay <53023+jayair@users.noreply.github.com>
2026-07-06 16:12:30 +08:00
Brendan AllanandGitHub 38bb38ecb2 refactor(app): unify provider connect dialog (#35518) 2026-07-06 07:50:08 +00:00
Luke ParkerandGitHub 3a149ba71c fix(app): optimize large review panes (#35375) 2026-07-06 16:26:03 +10:00
14df88eab5 fix(app): preserve provider dialog backdrop (#35370)
Co-authored-by: Jay <53023+jayair@users.noreply.github.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-07-06 06:08:59 +00:00
opencode-agent[bot] e12cb7fb6b chore: generate 2026-07-06 05:53:41 +00:00
Aiden ClineandGitHub d4f7039932 fix(codemode): unify catalog signatures (#35452) 2026-07-06 00:52:37 -05:00
opencode-agent[bot] e0ec9be238 chore: update nix node_modules hashes 2026-07-06 03:49:25 +00:00
Aiden ClineandGitHub 2b34df94fa fix(mcp): preserve metadata across tool pages (#35439) 2026-07-05 22:28:28 -05:00
68f225a11d fix(provider): preserve OpenRouter small model effort (#35478)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-07-05 18:50:25 -05:00
David HillandGitHub e9f5d34096 fix(tui): shorten home tips (#31966) 2026-07-05 14:44:10 -05:00
Aiden ClineandGitHub d3459eb740 test(mcp): replace module mocks with real servers (#35450) 2026-07-05 12:49:31 -05:00
Aiden ClineandGitHub be73f465df refactor(codemode): namespace public types (#35425) 2026-07-05 10:34:58 -05:00
Aiden ClineandGitHub f14eafe9db refactor(codemode): remove generic agent tool (#35417) 2026-07-05 09:30:46 -05:00
JackandGitHub b7e4f1ef74 fix: update Feishu community link (#35392) 2026-07-05 14:00:14 +08:00
Luke ParkerandGitHub efd5f0a80f fix(app): preserve timeline selection autoscroll (#35383) 2026-07-05 02:47:37 +00:00
opencode-agent[bot] 7135bc4fb5 chore: generate 2026-07-05 02:02:39 +00:00
7cf8d1ca6d feat(app): terminal improvements (#34747)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-07-05 02:01:30 +00:00
opencode-agent[bot] 78bca5c707 chore: generate 2026-07-05 01:39:09 +00:00
82db86e2a1 feat(desktop): reopen closed tabs and background tab open (#35010)
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
2026-07-05 01:37:59 +00:00
opencode-agent[bot] 476e3fc382 chore: generate 2026-07-05 00:51:28 +00:00
Luke ParkerandGitHub a12d50e15a fix(app): hydrate timeline message parents (#35269) 2026-07-05 10:49:46 +10:00
opencode-agent[bot] 1b9b260458 chore: generate 2026-07-04 21:18:39 +00:00
Aiden ClineandGitHub a8983bd2c7 feat(codemode): add OpenAPI tool adapter (#35192) 2026-07-04 16:17:30 -05:00
709af58612 fix(core): stop after declined permissions (#35356)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-07-04 16:17:11 -05:00
bcbbf32569 fix(app): use v2 tooltip for prompt context (#35351)
Co-authored-by: Jay <53023+jayair@users.noreply.github.com>
Co-authored-by: Jay <air@live.ca>
2026-07-04 14:54:46 -04:00
Simon KleeandGitHub 7a8e7c88f4 tui: preserve spinner registration (#35292) 2026-07-04 09:44:23 +02:00
Luke ParkerandGitHub a22676720d fix(app): match new session panel corners (#35257) 2026-07-04 04:08:15 +00:00
Luke ParkerandGitHub f63a451b6f fix(desktop): stabilize esm shim injection (#35270) 2026-07-04 13:31:13 +10:00
8b6a2b173b fix(app): delay initial tab preview (#35266)
Co-authored-by: Jay <53023+jayair@users.noreply.github.com>
2026-07-03 22:42:17 -04:00
Luke ParkerandGitHub 933dbfdb36 fix(app): restore unfocused composer typing (#35249) 2026-07-03 23:59:23 +00:00
opencode-agent[bot]GitHubAiden Clineopencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
b44bc0ad07 cleanup: tweak compaction prompt (#35220)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-07-03 15:26:03 -05:00
opencode-agent[bot] 436cd39d14 chore: generate 2026-07-03 18:42:23 +00:00
Aiden ClineandGitHub ed6dc879be feat(opencode): gate execute tool behind code mode flag (#35185) 2026-07-03 13:41:15 -05:00
James LongandGitHub 96d53c6716 refactor(core): move path resolve into fs service (#35202) 2026-07-03 13:25:25 -04:00
Aiden ClineandGitHub 911fb7063a fix(tui): align execute child calls with task indentation (#35190) 2026-07-03 12:06:58 -05:00
Aiden ClineandGitHub 27e8e2e22b refactor(opencode): fail the execute tool on program failure (#35180) 2026-07-03 11:27:36 -05:00
Aiden ClineandGitHub 39c6dd1c32 feat(tui): render code-mode execute tool with child calls (#35113) 2026-07-03 11:17:21 -05:00
opencode-agent[bot] a09447bc9b chore: generate 2026-07-03 15:40:10 +00:00
Adam 16815473a4 chore: artifacts 2026-07-03 10:38:49 -05:00
Adam 1f47bbfce2 fix(stats): link market share labs 2026-07-03 10:34:13 -05:00
James LongandGitHub 33ed95f09c fix(core): rewrite replacements while hoisting layers (#35175) 2026-07-03 11:26:49 -04:00
opencode-agent[bot] 2152624f37 chore: generate 2026-07-03 15:16:03 +00:00
Adam 7d84cb05ef feat(stats): add model momentum section 2026-07-03 10:13:56 -05:00
James LongandGitHub eba0bd0397 chore: upgrade turbo (#35173) 2026-07-03 10:41:17 -04:00
Kit LangtonandGitHub 9aab24e95e fix(opencode): provide Observability beneath all route service graphs (#35171) 2026-07-03 10:00:30 -04:00
a4fed69a82 feat(app): dropdown search fix (#34961)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-07-03 10:31:37 +00:00
opencode-agent[bot] 41a3cfcdd9 chore: update nix node_modules hashes 2026-07-03 09:37:53 +00:00
opencode-agent[bot] 72845e09fc chore: generate 2026-07-03 09:25:41 +00:00
Aiden ClineandGitHub abaab29cb3 feat(opencode): add code-mode MCP adapter (#35085) 2026-07-03 09:24:27 +00:00
opencode-agent[bot] 30936a9bca chore: generate 2026-07-03 08:11:01 +00:00
Aarav SareenandGitHub a9144eccf8 feat(app): improvements to model search (#34954) 2026-07-03 08:09:51 +00:00
Aiden ClineandGitHub 458ec7b372 refactor(opencode): expose MCP tools in native shape from the service (#35103) 2026-07-03 03:05:36 -05:00
usrnk1andGitHub d46c02ba73 feat(desktop): papercut fixes (#34939) 2026-07-03 08:04:59 +00:00
opencode-agent[bot] caedf36844 chore: generate 2026-07-03 07:54:40 +00:00
4c6e2a92ce feat(app): align subagent UI with v2 (#34931)
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
2026-07-03 15:53:31 +08:00
usrnk1andGitHub bf58fae51f feat(desktop): add recently closed projects to home (#34926) 2026-07-03 15:52:40 +08:00
Aiden ClineandGitHub eb3476660f chore: remove root node-gyp pin after Node 24 setup (#35092) 2026-07-03 00:45:17 -05:00
opencode-agent[bot] 7d48a22a63 chore: update nix node_modules hashes 2026-07-03 05:33:25 +00:00
opencode-agent[bot] 3f0fc22b6e chore: generate 2026-07-03 05:20:28 +00:00
Aiden ClineandGitHub 2409c7a3d5 feat(codemode): add confined execution package (#35079) 2026-07-03 05:19:11 +00:00
Aiden ClineandGitHub 379adee35c revert: back out experimental codemode (#35077) 2026-07-02 23:57:30 -05:00
Luke ParkerandGitHub 2ef1a5991c fix(ci): setup Node 24 before bun install for node-gyp (#35076) 2026-07-03 14:55:57 +10:00
opencode-agent[bot] 83c638eaac chore: generate 2026-07-03 04:49:44 +00:00
Aiden ClineandGitHub cb93114424 feat: experimental codemode (#34677) 2026-07-02 23:48:34 -05:00
Luke ParkerandGitHub 04d236ceed fix(app): keep v2 review pane mounted across session tab switches (#35074) 2026-07-03 14:22:20 +10:00
Frank 5455fed78e zen: new inference 2026-07-02 21:59:33 -04:00
Frank 6dba0cc498 zen: new inference 2026-07-02 21:56:52 -04:00
Luke ParkerandGitHub c07ac0db2f feat(app): navigate tabs on mousedown in new layout (#35042) 2026-07-03 11:47:06 +10:00
Frank 20b090e493 zen: new inference 2026-07-02 21:38:29 -04:00
Frank cb54824f2f zen: new inference 2026-07-02 21:28:38 -04:00
Dustin DeusandGitHub 3adfb970bf feat(tui): add debug info dialog with copy to clipboard (#35004) 2026-07-02 23:00:53 +02:00
opencode-agent[bot] 5ecd19db9f chore: generate 2026-07-02 19:43:04 +00:00
Adam 299daa2815 feat(stats): redesign model hero 2026-07-02 14:41:22 -05:00
Adam 7457139849 fix(stats): polish lab pages 2026-07-02 14:38:16 -05:00
Aiden ClineandGitHub 373cd08b98 fix(copilot): honor advertised model endpoints (#34958) 2026-07-02 10:23:57 -05:00
Luke ParkerandGitHub f52424e05f fix(ui): resolve toast icon inside the toast component (#34874) 2026-07-02 15:53:47 +08:00
4a42caef2c fix(app): keep terminal mounted when switching session tabs in a workspace (#34852)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-07-02 15:53:19 +08:00
7d2618637f feat(app): v2 review panel overhaul (#31882)
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
2026-07-02 07:41:58 +00:00
Luke ParkerandGitHub fbb95a6ee3 fix(app): seed session status before warming session info (#34864) 2026-07-02 16:32:25 +10:00
Brendan AllanandGitHub 40233595b0 fix(app): clear original prompt after retarget (#34863) 2026-07-02 06:24:53 +00:00
opencode-agent[bot] 403164a6ac chore: generate 2026-07-02 06:08:44 +00:00
Luke ParkerandGitHub 1fb2ecc721 fix(app): scope session tab indicators to the tab's server (#34861) 2026-07-02 16:07:35 +10:00
Luke ParkerandGitHub 27c9b130c7 test(opencode): cover Windows session list directory spellings (#34857) 2026-07-02 05:16:17 +00:00
Brendan AllanandGitHub 5fecf7ae9e fix(app): only allow \(...\) syntax for inline latex (#34850) 2026-07-02 13:02:17 +08:00
Luke ParkerandGitHub a0d64b12c9 fix(opencode): filter session list by resolved instance directory (#34842) 2026-07-02 04:39:49 +00:00
opencode-agent[bot] d6b897188a chore: generate 2026-07-02 04:25:52 +00:00
Luke ParkerandGitHub 39dfbb53d0 fix(app): resolve target session lineage outside router transition (#34838) 2026-07-02 14:24:39 +10:00
opencode-agent[bot] a39db9c6f6 chore: update nix node_modules hashes 2026-07-02 03:36:41 +00:00
6ca60d9204 fix(opencode): update Cerebras SDK reasoning replay (#34826)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
2026-07-01 22:22:27 -05:00
opencode-agent[bot] b3934992d1 chore: update nix node_modules hashes 2026-07-02 02:52:09 +00:00
opencode-agent[bot]GitHubJayopencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
4de7e90d8d fix(ui): raise v2 tooltip layer (#34455)
Co-authored-by: Jay <53023+jayair@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-07-02 10:45:13 +08:00
usrnk1andGitHub 917eca4f55 feat(desktop): refine session tab preview popover (#34792) 2026-07-02 10:41:25 +08:00
Luke ParkerandGitHub 3cf71808c4 fix(app): stabilize session timeline layout continuity (#34533) 2026-07-02 12:36:47 +10:00
Luke ParkerandGitHub f266e829cf fix(desktop): keep window tabs across app close (#34807) 2026-07-01 23:23:11 +00:00
Frank 3df5556223 zen: new inference 2026-07-01 17:54:22 -04:00
AdamandGitHub fc29f99190 fix(stats): include locale header in vary (#34789) 2026-07-01 15:25:49 -05:00
Adam beb586a383 fix(stats): refine lab page tooltips 2026-07-01 14:54:35 -05:00
opencode-agent[bot] 5668d36477 chore: generate 2026-07-01 19:45:26 +00:00
Adam 3706b33ee0 feat(stats): use catalog descriptions 2026-07-01 14:41:28 -05:00
Adam 85e8ac3b36 feat(data): another redesigned lab section 2026-07-01 14:41:28 -05:00
Adam 91ca75c4dc feat(stats): redesign lab usage 2026-07-01 14:41:28 -05:00
Adam 262ef4fdc2 feat(stats): add lab overview 2026-07-01 14:41:28 -05:00
Adam 74e5644646 feat(stats): redesign lab hero 2026-07-01 14:41:28 -05:00
opencode-agent[bot] 7de5aa08ff chore: update nix node_modules hashes 2026-07-01 15:40:08 +00:00
Vladimir GlafirovandGitHub 9495e3e903 chore: bump gitlab-ai-provider to 6.10.0 (#34756) 2026-07-01 10:23:07 -05:00
opencode 1e73b76ea6 sync release versions for v1.17.13 2026-07-01 15:19:04 +00:00
Aarav SareenandGitHub 6697cf3fd8 feat(app): minor fixes (#34748) 2026-07-01 21:10:53 +08:00
opencode-agent[bot] 87aa62d1e2 chore: generate 2026-07-01 12:41:15 +00:00
usrnk1andGitHub 3136b1ba97 feat(desktop): improve markdown heading spacing in session timeline (#34738) 2026-07-01 20:39:12 +08:00
246dea3a58 feat(app): more v2 ui alignment (#34465)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-07-01 12:14:51 +00:00
opencode-agent[bot] 4b1c7f28a0 chore: generate 2026-07-01 12:08:40 +00:00
9410eda4e3 feat(app): composer improvements (#34720)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-07-01 12:06:41 +00:00
f014686b07 feat(desktop): session tab hover preview popover (#34678)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
2026-07-01 17:16:45 +08:00
opencode-agent[bot] 8b68dc0d7f chore: generate 2026-07-01 09:05:06 +00:00
Brendan AllanandGitHub 2f4d36d64e fix(desktop): clean stale draft stores (#34651) 2026-07-01 09:03:27 +00:00
opencode-agent[bot] 6f2ff35656 chore: generate 2026-07-01 08:17:22 +00:00
cd00422298 fix(app): question UI fixes and UX improvements (#34116)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
2026-07-01 08:15:31 +00:00
opencode-agent[bot] 34ee94232f chore: generate 2026-07-01 07:40:25 +00:00
4d91f0cf28 feat(app): v2 wsl ui (#34233)
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
2026-07-01 07:39:00 +00:00
opencode-agent[bot] bf18cc971f chore: generate 2026-07-01 07:06:04 +00:00
Brendan AllanandGitHub 5e1a1ed2e2 fix(app): scope session page errors (#34254) 2026-07-01 15:04:18 +08:00
opencode-agent[bot] bd7f7ad98b chore: generate 2026-07-01 07:03:41 +00:00
Brendan AllanandGitHub af72cec799 feat(desktop): scope tabs to windows (#34669) 2026-07-01 07:02:10 +00:00
Aiden ClineandGitHub 55552c521f fix(provider): force openai reasoning variants (#34702) 2026-06-30 22:45:39 -05:00
opencode-agent[bot] 2b611a5b14 chore: update nix node_modules hashes 2026-07-01 01:44:56 +00:00
opencode-agent[bot]GitHubLukeParkerDevopencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
45a437cbb2 fix(app): clamp virtualizer range to scroll bounds (#34588)
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-07-01 11:28:34 +10:00
opencode-agent[bot] 439ec9b76f chore: generate 2026-07-01 00:53:45 +00:00
opencode-agent[bot]GitHubJayLuke Parkeropencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
ae3fefb42a fix(session-ui): recognize more inline file paths (#34688)
Co-authored-by: Jay <53023+jayair@users.noreply.github.com>
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-07-01 00:52:10 +00:00
Aiden ClineandGitHub f1407e41c4 fix(core): stop replaying stale GitHub Copilot Responses item IDs (#34686) 2026-06-30 19:19:02 -05:00
opencode-agent[bot] c88ca972cf chore: generate 2026-06-30 23:59:52 +00:00
Frank 928c37bc02 zen: kk2.7, mm3, sonnet5 2026-06-30 19:56:25 -04:00
opencode afff74eb2c sync release versions for v1.17.12 2026-06-30 19:48:01 +00:00
Aiden ClineandGitHub 3a669d528b fix(provider): enable sonnet 5 adaptive thinking (#34673) 2026-06-30 14:03:05 -05:00
JackandGitHub 6789214b31 fix(console): remove expired Go MiniMax promo (#34663) 2026-07-01 01:42:05 +08:00
James LongandGitHub 6636683323 refactor: remove remaining default layer aliases (#34660) 2026-06-30 13:17:34 -04:00
opencode-agent[bot] 3af9f64265 chore: generate 2026-06-30 17:07:39 +00:00
James LongandGitHub 5a23bdcb69 refactor(core): remove domain layer exports (#34625) 2026-06-30 13:05:15 -04:00
opencode-agent[bot] e78726854a chore: generate 2026-06-30 16:19:11 +00:00
James LongandGitHub 472d0f376e refactor(core): remove infrastructure layer exports (#34624) 2026-06-30 12:17:12 -04:00
Brendan AllanandGitHub c8fde60ad4 fix(app): hide missing workspace branch (#34649) 2026-07-01 00:05:09 +08:00
opencode-agent[bot]GitHubAiden Clineopencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
a4b6047e64 fix(core): drop legacy config filename (#34645)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-06-30 11:03:53 -05:00
James LongandGitHub 6387f955cf refactor(core): remove session layer exports (#34623) 2026-06-30 11:44:08 -04:00
James LongandGitHub 5691f36e98 refactor(core): remove tool layer exports (#34622) 2026-06-30 11:18:41 -04:00
opencode-agent[bot] 74e7a378bb chore: generate 2026-06-30 15:02:51 +00:00
usrnk1andGitHub fced9c5a2f feat(desktop): polish tooltips and session search (#34632) 2026-06-30 23:00:25 +08:00
opencode-agent[bot]GitHubJames Longopencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
20445ca031 fix(cli): hide auto approval aliases in help (#34641)
Co-authored-by: James Long <longster@gmail.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
2026-06-30 14:52:57 +00:00
James LongandGitHub 6fc9405618 refactor(core): route aggregate layers through nodes (#34621) 2026-06-30 10:46:44 -04:00
James LongandGitHub c86066efef test(opencode): update help snapshots (#34639) 2026-06-30 10:22:21 -04:00
usrnk1andGitHub fa9ba2938e feat(desktop): make error view draggable (#34627) 2026-06-30 22:12:43 +08:00
usrnk1andGitHub 63e24880e5 feat(desktop): polish inline session title editing (#34607) 2026-06-30 22:12:22 +08:00
opencode-agent[bot] eaf42c4ab0 chore: update nix node_modules hashes 2026-06-30 13:55:24 +00:00
Aiden ClineandGitHub 8e10ab0aa7 chore: bump venice provider (#34629) 2026-06-30 08:40:09 -05:00
750 changed files with 252565 additions and 118597 deletions
+4
View File
@@ -1,4 +1,5 @@
adamdotdevin
arvsrn
Brendonovich
fwang
Hona
@@ -7,11 +8,14 @@ jayair
jlongster
kitlangton
kommander
ludvigrask
MrMushrooooom
nexxeln
R44VC0RP
rekram1-node
thdxr
simonklee
Slickstef11
usrnk1
vimtor
starptech
+7
View File
@@ -8,6 +8,13 @@ inputs:
runs:
using: "composite"
steps:
# node-gyp@latest (invoked via bunx for native install scripts) requires Node >=22;
# some runner images ship an older system Node on PATH
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "24"
- name: Get baseline download URL
id: bun-url
shell: bash
+1 -1
View File
@@ -4,7 +4,7 @@ import { tool } from "@opencode-ai/plugin"
const TEAM = {
tui: ["kommander", "simonklee"],
desktop_web: ["Hona", "Brendonovich"],
core: ["jlongster", "rekram1-node", "nexxeln", "kitlangton", "starptech"],
core: ["jlongster", "rekram1-node", "nexxeln", "kitlangton"],
inference: ["fwang", "MrMushrooooom", "starptech"],
windows: ["Hona"],
} as const
+1 -1
View File
@@ -125,4 +125,4 @@ OpenCode 内置两种 Agent,可用 `Tab` 键快速切换:
---
**加入我们的社区** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=738j8655-cd59-4633-a30a-1124e0096789&qr_code=true) | [X.com](https://x.com/opencode)
**加入我们的社区** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=52ao9352-5623-4fa0-b7dd-3407c392c1af&qr_code=true) | [X.com](https://x.com/opencode)
+1 -1
View File
@@ -125,4 +125,4 @@ OpenCode 內建了兩種 Agent,您可以使用 `Tab` 鍵快速切換。
---
**加入我們的社群** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=738j8655-cd59-4633-a30a-1124e0096789&qr_code=true) | [X.com](https://x.com/opencode)
**加入我們的社群** [飞书](https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=52ao9352-5623-4fa0-b7dd-3407c392c1af&qr_code=true) | [X.com](https://x.com/opencode)
+3
View File
@@ -0,0 +1,3 @@
node_modules
.remotion
out/frame-*.png
+483
View File
@@ -0,0 +1,483 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "glm52-rise-video",
"dependencies": {
"@remotion/cli": "^4.0.384",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"remotion": "^4.0.384",
},
"devDependencies": {
"@types/react": "^19.2.8",
"@types/react-dom": "^19.2.3",
"typescript": "^5.8.2",
},
},
},
"packages": {
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
"@babel/parser": ["@babel/parser@7.24.1", "", { "bin": "./bin/babel-parser.js" }, "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg=="],
"@babel/types": ["@babel/types@7.24.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w=="],
"@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
"@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/source-map": ["@jridgewell/source-map@0.3.11", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@mediabunny/aac-encoder": ["@mediabunny/aac-encoder@1.47.0", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-JNzgdJoHMFFnv5imi1+dmjZMudsJ1zNCUCEkKjBh90cqGhAFg8xu4V2gsyxE2i6oq/YpWx23P+OvoANLSMcDzA=="],
"@mediabunny/flac-encoder": ["@mediabunny/flac-encoder@1.47.0", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-VpKmJO0xlYcFCRD6JvJlMbNQ6d/6YMHdO1gFIqWlZABHjSSL6BquNNEgWSCv5vdF8ELDBwIYBVZEslakIB/7GA=="],
"@mediabunny/mp3-encoder": ["@mediabunny/mp3-encoder@1.47.0", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-JyzZyGeGRm2HVUQaGJ/VZT9OG+kG00mA8SLP/f3CO7+qWAmBncKc16WYXWHbZEo8Jn/ZGA6De32S5R2bTQbDqA=="],
"@module-federation/error-codes": ["@module-federation/error-codes@0.22.0", "", {}, "sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug=="],
"@module-federation/runtime": ["@module-federation/runtime@0.22.0", "", { "dependencies": { "@module-federation/error-codes": "0.22.0", "@module-federation/runtime-core": "0.22.0", "@module-federation/sdk": "0.22.0" } }, "sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA=="],
"@module-federation/runtime-core": ["@module-federation/runtime-core@0.22.0", "", { "dependencies": { "@module-federation/error-codes": "0.22.0", "@module-federation/sdk": "0.22.0" } }, "sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA=="],
"@module-federation/runtime-tools": ["@module-federation/runtime-tools@0.22.0", "", { "dependencies": { "@module-federation/runtime": "0.22.0", "@module-federation/webpack-bundler-runtime": "0.22.0" } }, "sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA=="],
"@module-federation/sdk": ["@module-federation/sdk@0.22.0", "", {}, "sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g=="],
"@module-federation/webpack-bundler-runtime": ["@module-federation/webpack-bundler-runtime@0.22.0", "", { "dependencies": { "@module-federation/runtime": "0.22.0", "@module-federation/sdk": "0.22.0" } }, "sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="],
"@remotion/bundler": ["@remotion/bundler@4.0.483", "", { "dependencies": { "@remotion/media-parser": "4.0.483", "@remotion/studio": "4.0.483", "@remotion/studio-shared": "4.0.483", "@remotion/timeline-utils": "4.0.483", "@rspack/core": "1.7.11", "@rspack/plugin-react-refresh": "1.6.1", "css-loader": "7.1.4", "esbuild": "0.28.1", "react-refresh": "0.18.0", "remotion": "4.0.483", "style-loader": "4.0.0", "webpack": "5.105.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-5fQRMYgr2sxZPBOThnIVuFwHBq/FhusJ+Ke6xopga8io8ecK/mj9GaRXC3tgfMa+YWuu8ZSXZ0U9EsMZjjP5YQ=="],
"@remotion/canvas-capture": ["@remotion/canvas-capture@4.0.483", "", { "dependencies": { "mediabunny": "1.47.0", "remotion": "4.0.483" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-JABgbfvXwjFp1E61tlDN9gVAH8OoeF4kUUELNwG9kcNOE4ex8+vCYEO6VKIaXZ4l8A1PEAQ5EK//NheTJ80gfw=="],
"@remotion/cli": ["@remotion/cli@4.0.483", "", { "dependencies": { "@remotion/bundler": "4.0.483", "@remotion/media-utils": "4.0.483", "@remotion/player": "4.0.483", "@remotion/renderer": "4.0.483", "@remotion/studio": "4.0.483", "@remotion/studio-server": "4.0.483", "@remotion/studio-shared": "4.0.483", "dotenv": "17.3.1", "minimist": "1.2.6", "prompts": "2.4.2", "remotion": "4.0.483" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" }, "bin": { "remotion": "remotion-cli.js", "remotionb": "remotionb-cli.js", "remotiond": "remotiond-cli.js" } }, "sha512-z4TQcgTfQbSIV4GQpCvIihDL77M+leQelbxv962PmfSKTT7z3XnNN732PcZ4sei2+Xg+d2SD95K4tI5XBYs5Gw=="],
"@remotion/compositor-darwin-arm64": ["@remotion/compositor-darwin-arm64@4.0.483", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CkEJXouGEoCs5PX3KOufkXrlT/Kgn0ZF7SEQ4meCYGYMZD+QPVtsmS1MXH8pQ4RHlFO6Tk7g0CIHMT3Y4yzpuA=="],
"@remotion/compositor-darwin-x64": ["@remotion/compositor-darwin-x64@4.0.483", "", { "os": "darwin", "cpu": "x64" }, "sha512-vDZK5U8FYbHGMjFC2lUgJRBz8KXwbwFSvEQi8FATD0roNg75SoaEJWrqAOIVv8v5eBjPvo18znDc36NLRYn1Eg=="],
"@remotion/compositor-linux-arm64-gnu": ["@remotion/compositor-linux-arm64-gnu@4.0.483", "", { "os": "linux", "cpu": "arm64" }, "sha512-1LzfYDY/DQxy3MIc9pCbTn1ObcBDcyI6MzODOwe1bTZl8TZxdUUt1M3hDrQ60AB6iHwruBAnzT9dnogiMIdSew=="],
"@remotion/compositor-linux-arm64-musl": ["@remotion/compositor-linux-arm64-musl@4.0.483", "", { "os": "linux", "cpu": "arm64" }, "sha512-nG1btxg0HXuLzulIqAg9NnxWyewZNrA0iLe82HdniaJjeWLORreGo4cEEJz1SjbDpQuPEY562XGkTO2DLKK5mA=="],
"@remotion/compositor-linux-x64-gnu": ["@remotion/compositor-linux-x64-gnu@4.0.483", "", { "os": "linux", "cpu": "x64" }, "sha512-CV4h6r2rNYh4P8utUzP6RM2vW2vq43nA3IvVOwRSuh9LwJh/7rwG53Oco4HanY/sstaNuymGpT57wdXwONU5CQ=="],
"@remotion/compositor-linux-x64-musl": ["@remotion/compositor-linux-x64-musl@4.0.483", "", { "os": "linux", "cpu": "x64" }, "sha512-moKvavbkoz5nteJZA4K6LMgTTlewhwIbtO7DYARZ3LK3ClGUKnYfFcmaPS15hMun02JPTOMIWbrlaszdR75mxA=="],
"@remotion/compositor-win32-x64-msvc": ["@remotion/compositor-win32-x64-msvc@4.0.483", "", { "os": "win32", "cpu": "x64" }, "sha512-eDpWe6Vy7ySHugwxlgaeohqdBwR1etxiymXzT13cgbPw2+p6d55z1tJ7VPTXyfu0o+UdyRTauWqD4msoxN0wow=="],
"@remotion/licensing": ["@remotion/licensing@4.0.483", "", {}, "sha512-GRtjykrxMmB5Cjpq1oVqeYIRXI95vid6afX0LTLfp1shD6MR4lTZRoRuopxrrZp1E4XFGXoY64b6dlIudgTyBQ=="],
"@remotion/media-parser": ["@remotion/media-parser@4.0.483", "", {}, "sha512-iva9Eof7QQX+3hGxH/N8pGi80wgdGuzOr825Zn7IuEe7IWdtRYWlFpCEghmyfhrwgaWa7RY51wg77ycR3c3nRg=="],
"@remotion/media-utils": ["@remotion/media-utils@4.0.483", "", { "dependencies": { "mediabunny": "1.47.0", "remotion": "4.0.483" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4T4WQh/U95kCBzgmz/EL/HL8Zeox/AKAL5ufsehrZFgwlcvH1EsKNJH/RWaAtNPRLF+pskEOgFzPYaczHeI8hg=="],
"@remotion/player": ["@remotion/player@4.0.483", "", { "dependencies": { "remotion": "4.0.483" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-yyIQ9vinUqqS0fQeL52bPCWQIr8qDXCqQ4iys8P7dSZncdu8V29p4W2a0/4eKV8bv5+mjnJQ/U6nl9NxBF1hdA=="],
"@remotion/renderer": ["@remotion/renderer@4.0.483", "", { "dependencies": { "@remotion/licensing": "4.0.483", "@remotion/streaming": "4.0.483", "execa": "5.1.1", "remotion": "4.0.483", "source-map": "0.8.0-beta.0", "ws": "8.21.0" }, "optionalDependencies": { "@remotion/compositor-darwin-arm64": "4.0.483", "@remotion/compositor-darwin-x64": "4.0.483", "@remotion/compositor-linux-arm64-gnu": "4.0.483", "@remotion/compositor-linux-arm64-musl": "4.0.483", "@remotion/compositor-linux-x64-gnu": "4.0.483", "@remotion/compositor-linux-x64-musl": "4.0.483", "@remotion/compositor-win32-x64-msvc": "4.0.483" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-eSKdQqm8rcl+GNyOhFsBgcPYWMcxTJWHL5xaNrd037piCaL3sdJ57OEO/AbfEvvOO14zwUvlsONKjlkLlEVyKg=="],
"@remotion/streaming": ["@remotion/streaming@4.0.483", "", {}, "sha512-96rqrk+l5AfriHOqmqxMn8rI6UJfFY7r5UMDZoSKqXWzTOwj+0VWHTRl9ilZQMXhYACaMSA/5yrL9jgQEK8Yuw=="],
"@remotion/studio": ["@remotion/studio@4.0.483", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.31", "@remotion/canvas-capture": "4.0.483", "@remotion/media-utils": "4.0.483", "@remotion/player": "4.0.483", "@remotion/renderer": "4.0.483", "@remotion/studio-shared": "4.0.483", "@remotion/timeline-utils": "4.0.483", "@remotion/web-renderer": "4.0.483", "@remotion/zod-types": "4.0.483", "mediabunny": "1.47.0", "memfs": "3.4.3", "open": "8.4.2", "remotion": "4.0.483", "semver": "7.5.3", "zod": "4.3.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-gNa1Lw8NmHl0jwFb5fk7rzA+WFTJQpGDT/49A38t9jpEwoF+h1mKTgY8gA202apzjBHrEECdFE450npnBOUpUw=="],
"@remotion/studio-server": ["@remotion/studio-server@4.0.483", "", { "dependencies": { "@babel/parser": "7.24.1", "@babel/types": "7.24.0", "@remotion/bundler": "4.0.483", "@remotion/renderer": "4.0.483", "@remotion/studio-shared": "4.0.483", "memfs": "3.4.3", "open": "8.4.2", "prettier": "3.8.1", "recast": "0.23.11", "remotion": "4.0.483", "semver": "7.5.3" } }, "sha512-2FSLbVN5N8bgtQPX+RQRbis557tJJnVTOZuEBZndfC9NOoPdARlSgEnwVer9tBNczUlc6B3KrPxyPSlS3UjL+Q=="],
"@remotion/studio-shared": ["@remotion/studio-shared@4.0.483", "", { "dependencies": { "remotion": "4.0.483" } }, "sha512-aXxBYUgsWgphHVq7T7bz+ICMGzfii7pcX1IxS5hbLMLJHqlDtk9g+qcxm4kJ2Fjj/Gb5gERKiNzYym8WZTIM2Q=="],
"@remotion/timeline-utils": ["@remotion/timeline-utils@4.0.483", "", { "dependencies": { "mediabunny": "1.47.0" } }, "sha512-KTTMdpNA5uRLX+iisAITO+V9cJBk17F8EWvuUnAEVmAFOQTbGIyPI9HNw5pZmL7SlzOJdFjAtksTjipeYGkEug=="],
"@remotion/web-renderer": ["@remotion/web-renderer@4.0.483", "", { "dependencies": { "@mediabunny/aac-encoder": "1.47.0", "@mediabunny/flac-encoder": "1.47.0", "@mediabunny/mp3-encoder": "1.47.0", "@remotion/licensing": "4.0.483", "mediabunny": "1.47.0", "remotion": "4.0.483" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-14iURfVWA/hcF/rJvkid1ZR5nmnVhkTIf071FabCFi6kdViOSVBugSglJ8tUPvWVbWmxlXGDGQD9CSU18IATww=="],
"@remotion/zod-types": ["@remotion/zod-types@4.0.483", "", { "dependencies": { "remotion": "4.0.483" }, "peerDependencies": { "zod": "4.3.6" } }, "sha512-KIVYIzFpZBDB0wxGGQu5tDBr4XOYRzvrPXjmo6O/FI5Xx7O8hgvJmBs9VDkEwonAOft86sg79hI19F1DTX1p4w=="],
"@rspack/binding": ["@rspack/binding@1.7.11", "", { "optionalDependencies": { "@rspack/binding-darwin-arm64": "1.7.11", "@rspack/binding-darwin-x64": "1.7.11", "@rspack/binding-linux-arm64-gnu": "1.7.11", "@rspack/binding-linux-arm64-musl": "1.7.11", "@rspack/binding-linux-x64-gnu": "1.7.11", "@rspack/binding-linux-x64-musl": "1.7.11", "@rspack/binding-wasm32-wasi": "1.7.11", "@rspack/binding-win32-arm64-msvc": "1.7.11", "@rspack/binding-win32-ia32-msvc": "1.7.11", "@rspack/binding-win32-x64-msvc": "1.7.11" } }, "sha512-2MGdy2s2HimsDT444Bp5XnALzNRxuBNc7y0JzyuqKbHBywd4x2NeXyhWXXoxufaCFu5PBc9Qq9jyfjW2Aeh06Q=="],
"@rspack/binding-darwin-arm64": ["@rspack/binding-darwin-arm64@1.7.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oduECiZVqbO5zlVw+q7Vy65sJFth99fWPTyucwvLJJtJkPL5n17Uiql2cYP6Ijn0pkqtf1SXgK8WjiKLG5bIig=="],
"@rspack/binding-darwin-x64": ["@rspack/binding-darwin-x64@1.7.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-a1+TtTE9ap6RalgFi7FGIgkJP6O4Vy6ctv+9WGJy53E4kuqHR0RygzaiVxCI/GMc/vBT9vY23hyrpWb3d1vtXA=="],
"@rspack/binding-linux-arm64-gnu": ["@rspack/binding-linux-arm64-gnu@1.7.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-P0QrGRPbTWu6RKWfN0bDtbnEps3rXH0MWIMreZABoUrVmNQKtXR6e73J3ub6a+di5s2+K0M2LJ9Bh2/H4UsDUA=="],
"@rspack/binding-linux-arm64-musl": ["@rspack/binding-linux-arm64-musl@1.7.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-6ky7R43VMjWwmx3Yx7Jl7faLBBMAgMDt+/bN35RgwjiPgsIByz65EwytUVuW9rikB43BGHvA/eqlnjLrUzNBqw=="],
"@rspack/binding-linux-x64-gnu": ["@rspack/binding-linux-x64-gnu@1.7.11", "", { "os": "linux", "cpu": "x64" }, "sha512-cuOJMfCOvb2Wgsry5enXJ3iT1FGUjdPqtGUBVupQlEG4ntSYsQ2PtF4wIDVasR3wdxC5nQbipOrDiN/u6fYsdQ=="],
"@rspack/binding-linux-x64-musl": ["@rspack/binding-linux-x64-musl@1.7.11", "", { "os": "linux", "cpu": "x64" }, "sha512-CoK37hva4AmHGh3VCsQXmGr40L36m1/AdnN5LEjUX6kx5rEH7/1nEBN6Ii72pejqDVvk9anEROmPDiPw10tpFg=="],
"@rspack/binding-wasm32-wasi": ["@rspack/binding-wasm32-wasi@1.7.11", "", { "dependencies": { "@napi-rs/wasm-runtime": "1.0.7" }, "cpu": "none" }, "sha512-OtrmnPUVJMxjNa3eDMfHyPdtlLRmmp/aIm0fQHlAOATbZvlGm12q7rhPW5BXTu1yh+1rQ1/uqvz+SzKEZXuJaQ=="],
"@rspack/binding-win32-arm64-msvc": ["@rspack/binding-win32-arm64-msvc@1.7.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-lObFW6e5lCWNgTBNwT//yiEDbsxm9QG4BYUojqeXxothuzJ/L6ibXz6+gLMvbOvLGV3nKgkXmx8GvT9WDKR0mA=="],
"@rspack/binding-win32-ia32-msvc": ["@rspack/binding-win32-ia32-msvc@1.7.11", "", { "os": "win32", "cpu": "ia32" }, "sha512-0pYGnZd8PPqNR68zQ8skamqNAXEA1sUfXuAdYcknIIRq2wsbiwFzIc0Pov1cIfHYab37G7sSIPBiOUdOWF5Ivw=="],
"@rspack/binding-win32-x64-msvc": ["@rspack/binding-win32-x64-msvc@1.7.11", "", { "os": "win32", "cpu": "x64" }, "sha512-EeQXayoQk/uBkI3pdoXfQBXNIUrADq56L3s/DFyM2pJeUDrWmhfIw2UFIGkYPTMSCo8F2JcdcGM32FGJrSnU0Q=="],
"@rspack/core": ["@rspack/core@1.7.11", "", { "dependencies": { "@module-federation/runtime-tools": "0.22.0", "@rspack/binding": "1.7.11", "@rspack/lite-tapable": "1.1.0" }, "peerDependencies": { "@swc/helpers": ">=0.5.1" }, "optionalPeers": ["@swc/helpers"] }, "sha512-rsD9b+Khmot5DwCMiB3cqTQo53ioPG3M/A7BySu8+0+RS7GCxKm+Z+mtsjtG/vsu4Tn2tcqCdZtA3pgLoJB+ew=="],
"@rspack/lite-tapable": ["@rspack/lite-tapable@1.1.0", "", {}, "sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw=="],
"@rspack/plugin-react-refresh": ["@rspack/plugin-react-refresh@1.6.1", "", { "dependencies": { "error-stack-parser": "^2.1.4", "html-entities": "^2.6.0" }, "peerDependencies": { "react-refresh": ">=0.10.0 <1.0.0", "webpack-hot-middleware": "2.x" }, "optionalPeers": ["webpack-hot-middleware"] }, "sha512-eqqW5645VG3CzGzFgNg5HqNdHVXY+567PGjtDhhrM8t67caxmsSzRmT5qfoEIfBcGgFkH9vEg7kzXwmCYQdQDw=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
"@types/dom-mediacapture-transform": ["@types/dom-mediacapture-transform@0.1.11", "", { "dependencies": { "@types/dom-webcodecs": "*" } }, "sha512-Y2p+nGf1bF2XMttBnsVPHUWzRRZzqUoJAKmiP10b5umnO6DDrWI0BrGDJy1pOHoOULVmGSfFNkQrAlC5dcj6nQ=="],
"@types/dom-webcodecs": ["@types/dom-webcodecs@0.1.13", "", {}, "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ=="],
"@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="],
"@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg=="],
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="],
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="],
"@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="],
"@webassemblyjs/helper-api-error": ["@webassemblyjs/helper-api-error@1.13.2", "", {}, "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ=="],
"@webassemblyjs/helper-buffer": ["@webassemblyjs/helper-buffer@1.14.1", "", {}, "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA=="],
"@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.13.2", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA=="],
"@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.13.2", "", {}, "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA=="],
"@webassemblyjs/helper-wasm-section": ["@webassemblyjs/helper-wasm-section@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/wasm-gen": "1.14.1" } }, "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw=="],
"@webassemblyjs/ieee754": ["@webassemblyjs/ieee754@1.13.2", "", { "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw=="],
"@webassemblyjs/leb128": ["@webassemblyjs/leb128@1.13.2", "", { "dependencies": { "@xtuc/long": "4.2.2" } }, "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw=="],
"@webassemblyjs/utf8": ["@webassemblyjs/utf8@1.13.2", "", {}, "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ=="],
"@webassemblyjs/wasm-edit": ["@webassemblyjs/wasm-edit@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/helper-wasm-section": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-opt": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1", "@webassemblyjs/wast-printer": "1.14.1" } }, "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ=="],
"@webassemblyjs/wasm-gen": ["@webassemblyjs/wasm-gen@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg=="],
"@webassemblyjs/wasm-opt": ["@webassemblyjs/wasm-opt@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1" } }, "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw=="],
"@webassemblyjs/wasm-parser": ["@webassemblyjs/wasm-parser@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ=="],
"@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw=="],
"@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="],
"@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="],
"acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
"acorn-import-phases": ["acorn-import-phases@1.0.4", "", { "peerDependencies": { "acorn": "^8.14.0" } }, "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ=="],
"ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
"ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
"ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="],
"ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.40", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw=="],
"browserslist": ["browserslist@4.28.4", "", { "dependencies": { "baseline-browser-mapping": "^2.10.38", "caniuse-lite": "^1.0.30001799", "electron-to-chromium": "^1.5.376", "node-releases": "^2.0.48", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw=="],
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001799", "", {}, "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw=="],
"chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="],
"commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"css-loader": ["css-loader@7.1.4", "", { "dependencies": { "icss-utils": "^5.1.0", "postcss": "^8.4.40", "postcss-modules-extract-imports": "^3.1.0", "postcss-modules-local-by-default": "^4.0.5", "postcss-modules-scope": "^3.2.0", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", "semver": "^7.6.3" }, "peerDependencies": { "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", "webpack": "^5.27.0" }, "optionalPeers": ["@rspack/core", "webpack"] }, "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw=="],
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="],
"dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
"electron-to-chromium": ["electron-to-chromium@1.5.379", "", {}, "sha512-v/qV5aV5EUA2pGilzUCq5/eyOloZAqDZBu9UMBIzgPpLlprjSR6zswsWBTv0KpqxLGUAZEwhO95ZCt7srymNVA=="],
"enhanced-resolve": ["enhanced-resolve@5.24.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-7DdUaTjmNwMcH2gLr1qycesKII3BK4RLy/mdAb7x10Lq7bR4aNKHt1BR1ZALSv0rPM/hF5wYF0PhGop/rJm8vw=="],
"error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="],
"es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="],
"esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="],
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
"estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="],
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
"execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="],
"fs-monkey": ["fs-monkey@1.0.3", "", {}, "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q=="],
"get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
"glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"html-entities": ["html-entities@2.6.0", "", {}, "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="],
"human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
"icss-utils": ["icss-utils@5.1.0", "", { "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA=="],
"is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="],
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
"loader-runner": ["loader-runner@4.3.2", "", {}, "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w=="],
"lodash.sortby": ["lodash.sortby@4.7.0", "", {}, "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="],
"lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
"mediabunny": ["mediabunny@1.47.0", "", { "dependencies": { "@types/dom-mediacapture-transform": "^0.1.11", "@types/dom-webcodecs": "0.1.13" } }, "sha512-XQMZAcaKPkJ7hQ/Q2fvBdl3ZazQl2WVxDysUbJWh4PuAnLoerdsQBdPTDWdUdK6hh26LQ8Ue94MLLnmpWvlUYg=="],
"memfs": ["memfs@3.4.3", "", { "dependencies": { "fs-monkey": "1.0.3" } }, "sha512-eivjfi7Ahr6eQTn44nvTnR60e4a1Fs1Via2kCR5lHo/kyNoiMWaXCNJ/GpSd0ilXas2JSOl9B5FTIhflXu0hlg=="],
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
"minimist": ["minimist@1.2.6", "", {}, "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="],
"nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="],
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
"node-releases": ["node-releases@2.0.50", "", {}, "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg=="],
"npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
"postcss-modules-extract-imports": ["postcss-modules-extract-imports@3.1.0", "", { "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q=="],
"postcss-modules-local-by-default": ["postcss-modules-local-by-default@4.2.0", "", { "dependencies": { "icss-utils": "^5.0.0", "postcss-selector-parser": "^7.0.0", "postcss-value-parser": "^4.1.0" }, "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw=="],
"postcss-modules-scope": ["postcss-modules-scope@3.2.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA=="],
"postcss-modules-values": ["postcss-modules-values@4.0.0", "", { "dependencies": { "icss-utils": "^5.0.0" }, "peerDependencies": { "postcss": "^8.1.0" } }, "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ=="],
"postcss-selector-parser": ["postcss-selector-parser@7.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg=="],
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="],
"react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="],
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
"recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="],
"remotion": ["remotion@4.0.483", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-lf4xq4Twn75TQeTavFLrTE4zdck7EKBSx07ZrPv4om40Sist+3G0kq7NNzSjMeJ6G2Wx6vI+0oICD/vv4sGWtg=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="],
"semver": ["semver@7.5.3", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
"source-map": ["source-map@0.8.0-beta.0", "", { "dependencies": { "whatwg-url": "^7.0.0" } }, "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
"stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="],
"strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
"style-loader": ["style-loader@4.0.0", "", { "peerDependencies": { "webpack": "^5.27.0" } }, "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA=="],
"supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
"terser": ["terser@5.48.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q=="],
"terser-webpack-plugin": ["terser-webpack-plugin@5.6.1", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ=="],
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
"to-fast-properties": ["to-fast-properties@2.0.0", "", {}, "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog=="],
"tr46": ["tr46@1.0.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"watchpack": ["watchpack@2.5.2", "", { "dependencies": { "graceful-fs": "^4.1.2" } }, "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg=="],
"webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="],
"webpack": ["webpack@5.105.0", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.19.0", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.16", "watchpack": "^2.5.1", "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw=="],
"webpack-sources": ["webpack-sources@3.5.0", "", {}, "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ=="],
"whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"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=="],
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"css-loader/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
+24
View File
@@ -0,0 +1,24 @@
{
"name": "glm52-rise-video",
"private": true,
"type": "module",
"scripts": {
"render": "remotion render src/index.tsx GLM52Rise out/glm-52-broke-out.mp4 --codec h264 --pixel-format yuv420p",
"render:sheep": "remotion render src/index.tsx NZSheep out/nz-sheep.mp4 --codec h264 --pixel-format yuv420p",
"render:novel": "remotion render src/index.tsx NovelTokens out/novel-1984.mp4 --codec h264 --pixel-format yuv420p",
"render:flash": "remotion render src/index.tsx FlashShare out/flash-share.mp4 --codec h264 --pixel-format yuv420p",
"render:minimax": "remotion render src/index.tsx MiniMaxClimb out/minimax-climb.mp4 --codec h264 --pixel-format yuv420p",
"still:june": "remotion still src/index.tsx JuneTotals out/june-totals.png --frame=0"
},
"dependencies": {
"@remotion/cli": "^4.0.384",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"remotion": "^4.0.384"
},
"devDependencies": {
"@types/react": "^19.2.8",
"@types/react-dom": "^19.2.3",
"typescript": "^5.8.2"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

+156
View File
@@ -0,0 +1,156 @@
// Verified against the production opencode-stats PlanetScale DB.
// Scope: tier='Go' (OpenCode Go), dataset='zen', client='all', source='all', grain='day'.
// metric = total_tokens. Daily token volume per model; GLM-5.2 (zhipu) launched Jun 17.
// Segments per day (tokens): glm-5.2, deepseek-v4-flash, deepseek-v4-pro, minimax-m3, all others.
export type Day = {
date: string
total: number
glm: number
dsf: number
dsp: number
mm: number
others: number
}
export const days: Day[] = [
{
date: "Jun 12",
total: 2_283_799_449_383,
glm: 0,
dsf: 1_176_701_653_509,
dsp: 569_527_034_307,
mm: 159_016_250_684,
others: 378_554_510_883,
},
{
date: "Jun 13",
total: 2_008_462_388_420,
glm: 0,
dsf: 995_338_131_997,
dsp: 445_817_536_548,
mm: 211_743_241_967,
others: 355_563_477_908,
},
{
date: "Jun 14",
total: 2_007_785_405_251,
glm: 0,
dsf: 983_954_176_228,
dsp: 428_151_999_341,
mm: 262_476_527_930,
others: 333_202_701_752,
},
{
date: "Jun 15",
total: 2_694_736_103_062,
glm: 0,
dsf: 1_255_893_953_859,
dsp: 632_223_338_376,
mm: 352_507_442_991,
others: 454_111_367_836,
},
{
date: "Jun 16",
total: 2_838_153_758_908,
glm: 0,
dsf: 1_336_625_283_800,
dsp: 676_480_415_730,
mm: 305_268_829_013,
others: 519_779_230_365,
},
{
date: "Jun 17",
total: 2_778_964_711_109,
glm: 70_095_977_043,
dsf: 1_339_831_523_555,
dsp: 660_414_395_220,
mm: 251_302_096_157,
others: 457_320_719_134,
},
{
date: "Jun 18",
total: 2_806_992_430_656,
glm: 201_130_231_172,
dsf: 1_295_599_996_869,
dsp: 595_665_008_776,
mm: 322_205_104_324,
others: 392_392_089_515,
},
{
date: "Jun 19",
total: 2_419_611_630_232,
glm: 199_086_413_910,
dsf: 1_115_750_468_802,
dsp: 475_965_869_304,
mm: 303_586_698_735,
others: 325_222_179_481,
},
{
date: "Jun 20",
total: 2_188_278_916_865,
glm: 193_931_516_396,
dsf: 1_050_194_681_012,
dsp: 395_303_435_278,
mm: 281_998_000_337,
others: 266_851_283_842,
},
{
date: "Jun 21",
total: 2_042_309_961_344,
glm: 181_894_043_118,
dsf: 985_164_570_580,
dsp: 368_194_079_542,
mm: 259_812_551_324,
others: 247_244_716_780,
},
{
date: "Jun 22",
total: 2_893_934_325_663,
glm: 301_759_048_475,
dsf: 1_298_124_282_989,
dsp: 581_012_596_194,
mm: 371_581_117_839,
others: 341_457_280_166,
},
{
date: "Jun 23",
total: 3_109_009_321_480,
glm: 282_277_235_158,
dsf: 1_423_571_678_821,
dsp: 627_374_654_587,
mm: 429_416_300_508,
others: 346_369_452_406,
},
{
date: "Jun 24",
total: 2_939_149_971_595,
glm: 256_497_442_533,
dsf: 1_373_583_023_234,
dsp: 601_270_997_775,
mm: 391_586_493_231,
others: 316_212_014_822,
},
{
date: "Jun 25",
total: 3_029_641_552_948,
glm: 256_279_657_734,
dsf: 1_481_084_002_776,
dsp: 602_077_167_287,
mm: 375_985_302_874,
others: 314_215_422_277,
},
]
export const launchIndex = 5 // Jun 17, first day of GLM-5.2 usage
// GLM-5.2 weekly token volume, Jun 19-25 (sum of glm): 1,671,725,357,324 = 1.67T
export const glmWeekTokensT = 1.672
// stacked segments, bottom -> top. GLM-5.2 is the hero (blue); the rest are the field it cut into.
export const segments = [
{ key: "glm", label: "GLM-5.2", color: "#3b5cf6", hero: true },
{ key: "dsf", label: "deepseek-v4-flash", color: "#9ca3ad" },
{ key: "dsp", label: "deepseek-v4-pro", color: "#b3b9c1" },
{ key: "mm", label: "minimax-m3", color: "#c8cdd3" },
{ key: "others", label: "other models", color: "#dde0e4" },
] as const
+185
View File
@@ -0,0 +1,185 @@
import React from "react"
import { AbsoluteFill, Easing, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"
// stats.opencode.ai design tokens (light theme)
const c = {
bg: "#ffffff",
ink: "#161616",
muted: "#5c5c5c",
faint: "#808080",
line: "#e6e6e6",
dot: "#e4e4e4",
gray: "#aab0b8",
accent: "#3b5cf6",
accentHi: "#5b78ff",
}
const MONO = '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
const DOT_MASK =
"url(\"data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E\")"
// verified: OpenCode Go, week of Jun 22-28, 2026 (2026-W26). share = % of 19.64T total Go tokens.
const bars = [
{ label: "deepseek-v4-flash", share: 48.3, hero: true },
{ label: "deepseek-v4-pro", share: 19.4 },
{ label: "minimax-m3", share: 13.0 },
{ label: "glm-5.2", share: 8.3 },
{ label: "mimo-v2.5", share: 4.3 },
{ label: "kimi-k2.7-code", share: 2.6 },
{ label: "other models", share: 4.1 },
]
function DataWordmark({ height = 30 }: { height?: number }) {
return (
<svg width={(height * 66) / 20} height={height} viewBox="0 0 66 20" fill="none" style={{ color: c.ink }}>
<path opacity="0.2" d="M12 16H4V8H12V16Z" fill="currentColor" />
<path d="M12 4H4V16H12V4ZM16 20H0V0H16V20Z" fill="currentColor" />
<path
d="M63.3543 16L62.5119 12.8711H58.6437L57.8013 16H55.7383L59.2454 4H61.9618L65.4689 16H63.3543ZM61.0678 7.851L60.6896 5.94269H60.4489L60.0707 7.851L59.1595 11.1347H61.9962L61.0678 7.851Z"
fill="currentColor"
/>
<path d="M52.5951 5.87392V16H50.4461V5.87392H47.4375V4H55.6209V5.87392H52.5951Z" fill="currentColor" />
<path
d="M45.2059 16L44.3635 12.8711H40.4953L39.6529 16H37.5898L41.097 4H43.8133L47.3205 16H45.2059ZM42.9194 7.851L42.5411 5.94269H42.3004L41.9222 7.851L41.011 11.1347H43.8477L42.9194 7.851Z"
fill="currentColor"
/>
<path
d="M28 4H32.0917C32.8138 4 33.4556 4.11461 34.0172 4.34384C34.5903 4.5616 35.0716 4.9169 35.4613 5.40974C35.8625 5.89112 36.1662 6.51003 36.3725 7.26648C36.5788 8.02292 36.6819 8.9341 36.6819 10C36.6819 11.0659 36.5788 11.9771 36.3725 12.7335C36.1662 13.49 35.8625 14.1146 35.4613 14.6075C35.0716 15.0888 34.5903 15.4441 34.0172 15.6734C33.4556 15.8911 32.8138 16 32.0917 16H28V4ZM32.0917 14.1261C32.8252 14.1261 33.3926 13.9026 33.7937 13.4556C34.1948 12.9971 34.3954 12.3152 34.3954 11.4097V8.59026C34.3954 7.68481 34.1948 7.0086 33.7937 6.5616C33.3926 6.10315 32.8252 5.87392 32.0917 5.87392H30.149V14.1261H32.0917Z"
fill="currentColor"
/>
</svg>
)
}
export function FlashShare() {
const frame = useCurrentFrame()
const { fps } = useVideoConfig()
const grow = (i: number) =>
Math.min(
1,
Math.max(0, spring({ frame: frame - 18 - i * 7, fps, config: { damping: 18, stiffness: 120, mass: 0.6 } })),
)
return (
<AbsoluteFill style={{ background: c.bg, color: c.ink, fontFamily: MONO, padding: 72, boxSizing: "border-box" }}>
<div style={{ height: "100%", display: "flex", flexDirection: "column" }}>
{/* header */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<DataWordmark height={30} />
<div style={{ fontSize: 20, fontWeight: 500, color: c.faint, letterSpacing: 1 }}>JUN 2228, 2026</div>
</div>
{/* headline (static) */}
<div style={{ marginTop: 50 }}>
<div style={{ fontSize: 23, fontWeight: 600, color: c.muted, letterSpacing: 2 }}>
OPENCODE GO · SHARE OF TOKENS
</div>
<div
style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 24, marginTop: 14 }}
>
<div style={{ fontSize: 62, fontWeight: 600, letterSpacing: -2, lineHeight: 1 }}>DeepSeek V4 Flash</div>
<div
style={{
fontSize: 88,
fontWeight: 600,
letterSpacing: -2,
lineHeight: 1,
color: c.accent,
fontVariantNumeric: "tabular-nums",
}}
>
48%
</div>
</div>
</div>
{/* bar chart */}
<div style={{ flex: 1, display: "flex", flexDirection: "column", justifyContent: "center", gap: 16 }}>
{bars.map((b, i) => {
const g = grow(i)
const pct = b.share * g
return (
<div key={b.label} style={{ display: "flex", alignItems: "center", gap: 18 }}>
<div
style={{
width: 268,
fontSize: 23,
fontWeight: b.hero ? 600 : 500,
color: b.hero ? c.ink : c.muted,
textAlign: "right",
whiteSpace: "nowrap",
}}
>
{b.label}
</div>
<div style={{ position: "relative", flex: 1, height: 48 }}>
{/* dotted 100% track — height is a multiple of the 12px tile, anchored bottom, so dots never clip */}
<div
style={{
position: "absolute",
inset: 0,
background: c.dot,
WebkitMaskImage: DOT_MASK,
maskImage: DOT_MASK,
WebkitMaskSize: "12px 12px",
maskSize: "12px 12px",
WebkitMaskRepeat: "repeat",
maskRepeat: "repeat",
WebkitMaskPosition: "left bottom",
maskPosition: "left bottom",
}}
/>
{/* fill */}
<div
style={{
position: "absolute",
left: 0,
top: 0,
bottom: 0,
width: `${pct}%`,
background: b.hero ? c.accent : c.gray,
borderRight: b.hero ? `2px solid ${c.accentHi}` : "none",
}}
/>
</div>
<div
style={{
width: 84,
fontSize: 24,
fontWeight: b.hero ? 600 : 500,
color: b.hero ? c.accent : c.muted,
textAlign: "right",
fontVariantNumeric: "tabular-nums",
}}
>
{Math.round(pct)}%
</div>
</div>
)
})}
</div>
{/* footer */}
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginTop: 24,
paddingTop: 22,
borderTop: `1px solid ${c.line}`,
fontSize: 20,
fontWeight: 500,
}}
>
<div style={{ display: "inline-flex", alignItems: "center", gap: 9, color: c.muted }}>
<span style={{ width: 13, height: 13, background: c.accent, display: "inline-block" }} />
DeepSeek V4 Flash · 9.48T tokens · 83.6M requests
</div>
<div style={{ color: c.ink }}>opencode.ai/data</div>
</div>
</div>
</AbsoluteFill>
)
}
+36
View File
@@ -0,0 +1,36 @@
import { Composition, registerRoot } from "remotion"
import { GLM52Rise } from "./video"
import { NZSheep } from "./sheep"
import { NovelTokens } from "./novel"
import { FlashShare } from "./flash"
import { MiniMaxClimb } from "./minimax"
import { JuneTotals } from "./june"
function Root() {
return (
<>
<Composition id="GLM52Rise" component={GLM52Rise} durationInFrames={240} fps={30} width={1080} height={1080} />
<Composition id="NZSheep" component={NZSheep} durationInFrames={150} fps={30} width={1080} height={1080} />
<Composition
id="NovelTokens"
component={NovelTokens}
durationInFrames={150}
fps={30}
width={1080}
height={1080}
/>
<Composition id="FlashShare" component={FlashShare} durationInFrames={165} fps={30} width={1080} height={1080} />
<Composition
id="MiniMaxClimb"
component={MiniMaxClimb}
durationInFrames={165}
fps={30}
width={1080}
height={1080}
/>
<Composition id="JuneTotals" component={JuneTotals} durationInFrames={1} fps={30} width={1080} height={1080} />
</>
)
}
registerRoot(Root)
+144
View File
@@ -0,0 +1,144 @@
import React from "react"
import { AbsoluteFill } from "remotion"
const c = {
bg: "#ffffff",
ink: "#161616",
muted: "#5c5c5c",
faint: "#808080",
line: "#e6e6e6",
dot: "#dcdcdc",
accent: "#3b5cf6",
}
const MONO = '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
const DOT_MASK =
"url(\"data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E\")"
// verified: OpenCode Go (tier=Go, dataset=zen), June 1-30, 2026.
// 72.78T tokens · 651.4M requests · 11.42M sessions -> rounded headline figures.
function DataWordmark({ height = 30 }: { height?: number }) {
return (
<svg width={(height * 66) / 20} height={height} viewBox="0 0 66 20" fill="none" style={{ color: c.ink }}>
<path opacity="0.2" d="M12 16H4V8H12V16Z" fill="currentColor" />
<path d="M12 4H4V16H12V4ZM16 20H0V0H16V20Z" fill="currentColor" />
<path
d="M63.3543 16L62.5119 12.8711H58.6437L57.8013 16H55.7383L59.2454 4H61.9618L65.4689 16H63.3543ZM61.0678 7.851L60.6896 5.94269H60.4489L60.0707 7.851L59.1595 11.1347H61.9962L61.0678 7.851Z"
fill="currentColor"
/>
<path d="M52.5951 5.87392V16H50.4461V5.87392H47.4375V4H55.6209V5.87392H52.5951Z" fill="currentColor" />
<path
d="M45.2059 16L44.3635 12.8711H40.4953L39.6529 16H37.5898L41.097 4H43.8133L47.3205 16H45.2059ZM42.9194 7.851L42.5411 5.94269H42.3004L41.9222 7.851L41.011 11.1347H43.8477L42.9194 7.851Z"
fill="currentColor"
/>
<path
d="M28 4H32.0917C32.8138 4 33.4556 4.11461 34.0172 4.34384C34.5903 4.5616 35.0716 4.9169 35.4613 5.40974C35.8625 5.89112 36.1662 6.51003 36.3725 7.26648C36.5788 8.02292 36.6819 8.9341 36.6819 10C36.6819 11.0659 36.5788 11.9771 36.3725 12.7335C36.1662 13.49 35.8625 14.1146 35.4613 14.6075C35.0716 15.0888 34.5903 15.4441 34.0172 15.6734C33.4556 15.8911 32.8138 16 32.0917 16H28V4ZM32.0917 14.1261C32.8252 14.1261 33.3926 13.9026 33.7937 13.4556C34.1948 12.9971 34.3954 12.3152 34.3954 11.4097V8.59026C34.3954 7.68481 34.1948 7.0086 33.7937 6.5616C33.3926 6.10315 32.8252 5.87392 32.0917 5.87392H30.149V14.1261H32.0917Z"
fill="currentColor"
/>
</svg>
)
}
function DotBand() {
return (
<div
style={{
height: 12,
background: c.dot,
WebkitMaskImage: DOT_MASK,
maskImage: DOT_MASK,
WebkitMaskSize: "12px 12px",
maskSize: "12px 12px",
WebkitMaskRepeat: "repeat",
maskRepeat: "repeat",
WebkitMaskPosition: "left top",
maskPosition: "left top",
}}
/>
)
}
function Metric({ value, label }: { value: string; label: string }) {
return (
<div>
<div
style={{
fontSize: 92,
fontWeight: 600,
letterSpacing: -3,
lineHeight: 1,
fontVariantNumeric: "tabular-nums",
marginLeft: -8, // align the glyph's visual left edge to the column
}}
>
{value}
</div>
<div style={{ marginTop: 16, fontSize: 24, fontWeight: 500, color: c.muted, letterSpacing: 1 }}>{label}</div>
</div>
)
}
export function JuneTotals() {
return (
<AbsoluteFill style={{ background: c.bg, color: c.ink, fontFamily: MONO, padding: 72, boxSizing: "border-box" }}>
<div style={{ height: "100%", display: "flex", flexDirection: "column" }}>
{/* header */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 22 }}>
<DataWordmark height={30} />
<div style={{ fontSize: 20, fontWeight: 500, color: c.faint, letterSpacing: 1 }}>MONTHLY RECAP</div>
</div>
<DotBand />
{/* hero */}
<div style={{ flex: 1, display: "flex", flexDirection: "column", justifyContent: "center" }}>
<div style={{ fontSize: 25, fontWeight: 600, color: c.muted, letterSpacing: 3 }}>OPENCODE GO · JUNE 2026</div>
<div style={{ marginTop: 14 }}>
<div
style={{
fontSize: 300,
fontWeight: 600,
letterSpacing: -10,
lineHeight: 0.82,
color: c.accent,
marginLeft: -22,
}}
>
73T
</div>
<div style={{ fontSize: 50, fontWeight: 600, color: c.ink, marginTop: 10 }}>tokens processed</div>
</div>
{/* supporting metrics */}
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
marginTop: 60,
paddingTop: 40,
borderTop: `1px solid ${c.line}`,
}}
>
<Metric value="650M" label="requests" />
<Metric value="11M" label="sessions" />
</div>
</div>
{/* footer */}
<DotBand />
<div
style={{
display: "flex",
justifyContent: "flex-end",
alignItems: "center",
marginTop: 22,
fontSize: 20,
fontWeight: 500,
}}
>
<div style={{ color: c.ink }}>opencode.ai/data</div>
</div>
</div>
</AbsoluteFill>
)
}
+201
View File
@@ -0,0 +1,201 @@
import React from "react"
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion"
const c = {
bg: "#ffffff",
ink: "#161616",
muted: "#5c5c5c",
faint: "#808080",
line: "#e6e6e6",
dot: "#e4e4e4",
accent: "#3b5cf6",
accentHi: "#5b78ff",
accentDim: "#aebcf3",
}
const MONO = '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
const DOT_MASK =
"url(\"data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E\")"
// verified: minimax-m3, OpenCode Go (tier=Go, dataset=zen), weekly total_tokens.
// W26 2.559T is +23.2% / +482.3B vs W25 2.077T.
const weeks = [
{ label: "May 25", t: 0.008 },
{ label: "Jun 1", t: 0.429 },
{ label: "Jun 8", t: 1.192 },
{ label: "Jun 15", t: 2.077 },
{ label: "Jun 22", t: 2.559, latest: true },
]
const AXIS_MAX = 3.0
function DataWordmark({ height = 30 }: { height?: number }) {
return (
<svg width={(height * 66) / 20} height={height} viewBox="0 0 66 20" fill="none" style={{ color: c.ink }}>
<path opacity="0.2" d="M12 16H4V8H12V16Z" fill="currentColor" />
<path d="M12 4H4V16H12V4ZM16 20H0V0H16V20Z" fill="currentColor" />
<path
d="M63.3543 16L62.5119 12.8711H58.6437L57.8013 16H55.7383L59.2454 4H61.9618L65.4689 16H63.3543ZM61.0678 7.851L60.6896 5.94269H60.4489L60.0707 7.851L59.1595 11.1347H61.9962L61.0678 7.851Z"
fill="currentColor"
/>
<path d="M52.5951 5.87392V16H50.4461V5.87392H47.4375V4H55.6209V5.87392H52.5951Z" fill="currentColor" />
<path
d="M45.2059 16L44.3635 12.8711H40.4953L39.6529 16H37.5898L41.097 4H43.8133L47.3205 16H45.2059ZM42.9194 7.851L42.5411 5.94269H42.3004L41.9222 7.851L41.011 11.1347H43.8477L42.9194 7.851Z"
fill="currentColor"
/>
<path
d="M28 4H32.0917C32.8138 4 33.4556 4.11461 34.0172 4.34384C34.5903 4.5616 35.0716 4.9169 35.4613 5.40974C35.8625 5.89112 36.1662 6.51003 36.3725 7.26648C36.5788 8.02292 36.6819 8.9341 36.6819 10C36.6819 11.0659 36.5788 11.9771 36.3725 12.7335C36.1662 13.49 35.8625 14.1146 35.4613 14.6075C35.0716 15.0888 34.5903 15.4441 34.0172 15.6734C33.4556 15.8911 32.8138 16 32.0917 16H28V4ZM32.0917 14.1261C32.8252 14.1261 33.3926 13.9026 33.7937 13.4556C34.1948 12.9971 34.3954 12.3152 34.3954 11.4097V8.59026C34.3954 7.68481 34.1948 7.0086 33.7937 6.5616C33.3926 6.10315 32.8252 5.87392 32.0917 5.87392H30.149V14.1261H32.0917Z"
fill="currentColor"
/>
</svg>
)
}
const CHART_H = 420 // multiple of 12 so the dotted tracks never clip
export function MiniMaxClimb() {
const frame = useCurrentFrame()
const { fps } = useVideoConfig()
const grow = (i: number) =>
Math.min(
1,
Math.max(0, spring({ frame: frame - 22 - i * 9, fps, config: { damping: 18, stiffness: 110, mass: 0.6 } })),
)
return (
<AbsoluteFill style={{ background: c.bg, color: c.ink, fontFamily: MONO, padding: 72, boxSizing: "border-box" }}>
<div style={{ height: "100%", display: "flex", flexDirection: "column" }}>
{/* header */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<DataWordmark height={30} />
<div style={{ fontSize: 20, fontWeight: 500, color: c.faint, letterSpacing: 1 }}>JUN 2228, 2026</div>
</div>
{/* headline (static) */}
<div style={{ marginTop: 50 }}>
<div style={{ fontSize: 23, fontWeight: 600, color: c.muted, letterSpacing: 2 }}>
OPENCODE GO · WEEKLY TOKENS
</div>
<div
style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 24, marginTop: 14 }}
>
<div style={{ fontSize: 62, fontWeight: 600, letterSpacing: -2, lineHeight: 1 }}>MiniMax M3</div>
<div style={{ textAlign: "right", flexShrink: 0 }}>
<div
style={{
fontSize: 84,
fontWeight: 600,
letterSpacing: -2,
lineHeight: 1,
color: c.accent,
fontVariantNumeric: "tabular-nums",
}}
>
+23.2%
</div>
<div style={{ fontSize: 19, fontWeight: 500, color: c.muted, letterSpacing: 1, marginTop: 6 }}>
WEEK OVER WEEK
</div>
</div>
</div>
</div>
{/* column chart */}
<div style={{ flex: 1, display: "flex", alignItems: "flex-end", marginTop: 30 }}>
<div style={{ width: "100%", display: "flex", alignItems: "flex-end", gap: 30 }}>
{weeks.map((w, i) => {
const g = grow(i)
const h = Math.round((w.t / AXIS_MAX) * CHART_H * g)
return (
<div key={w.label} style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center" }}>
{/* value + bar */}
<div style={{ position: "relative", width: "100%", height: CHART_H }}>
{/* dotted track */}
<div
style={{
position: "absolute",
inset: 0,
background: c.dot,
WebkitMaskImage: DOT_MASK,
maskImage: DOT_MASK,
WebkitMaskSize: "12px 12px",
maskSize: "12px 12px",
WebkitMaskRepeat: "repeat",
maskRepeat: "repeat",
WebkitMaskPosition: "left bottom",
maskPosition: "left bottom",
}}
/>
{/* fill */}
<div
style={{
position: "absolute",
left: 0,
right: 0,
bottom: 0,
height: h,
background: w.latest ? c.accent : c.accentDim,
borderTop: w.latest ? `3px solid ${c.accentHi}` : "none",
}}
/>
{/* value label */}
<div
style={{
position: "absolute",
left: 0,
right: 0,
bottom: h + 10,
textAlign: "center",
fontSize: 26,
fontWeight: w.latest ? 600 : 500,
color: w.latest ? c.accent : c.muted,
fontVariantNumeric: "tabular-nums",
opacity: interpolate(g, [0.5, 1], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
}),
}}
>
{w.t.toFixed(2)}T
</div>
</div>
{/* week label */}
<div
style={{
marginTop: 14,
fontSize: 18,
fontWeight: 500,
color: w.latest ? c.ink : c.faint,
}}
>
{w.label}
</div>
</div>
)
})}
</div>
</div>
{/* footer */}
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginTop: 22,
paddingTop: 22,
borderTop: `1px solid ${c.line}`,
fontSize: 20,
fontWeight: 500,
}}
>
<div style={{ display: "inline-flex", alignItems: "center", gap: 9, color: c.muted }}>
<span style={{ width: 13, height: 13, background: c.accent, display: "inline-block" }} />
2.56T tokens last week · +482.3B added
</div>
<div style={{ color: c.ink }}>opencode.ai/data</div>
</div>
</div>
</AbsoluteFill>
)
}
+135
View File
@@ -0,0 +1,135 @@
import React from "react"
import { AbsoluteFill, Easing, Img, interpolate, staticFile, useCurrentFrame } from "remotion"
const c = {
white: "#ffffff",
dim: "rgba(255,255,255,0.74)",
}
const MONO = '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
// verified: OpenCode Go, week of Jun 22-28, 2026 (2026-W26)
// 19,642,742,937,105 tokens / 173,651,197 requests = 113,116 tokens/request
const AVG = 113116
const K = Math.round(AVG / 1000) // 113
const nf = new Intl.NumberFormat("en-US")
function DataWordmark({ height = 30 }: { height?: number }) {
return (
<svg width={(height * 66) / 20} height={height} viewBox="0 0 66 20" fill="none" style={{ color: c.white }}>
<path opacity="0.35" d="M12 16H4V8H12V16Z" fill="currentColor" />
<path d="M12 4H4V16H12V4ZM16 20H0V0H16V20Z" fill="currentColor" />
<path
d="M63.3543 16L62.5119 12.8711H58.6437L57.8013 16H55.7383L59.2454 4H61.9618L65.4689 16H63.3543ZM61.0678 7.851L60.6896 5.94269H60.4489L60.0707 7.851L59.1595 11.1347H61.9962L61.0678 7.851Z"
fill="currentColor"
/>
<path d="M52.5951 5.87392V16H50.4461V5.87392H47.4375V4H55.6209V5.87392H52.5951Z" fill="currentColor" />
<path
d="M45.2059 16L44.3635 12.8711H40.4953L39.6529 16H37.5898L41.097 4H43.8133L47.3205 16H45.2059ZM42.9194 7.851L42.5411 5.94269H42.3004L41.9222 7.851L41.011 11.1347H43.8477L42.9194 7.851Z"
fill="currentColor"
/>
<path
d="M28 4H32.0917C32.8138 4 33.4556 4.11461 34.0172 4.34384C34.5903 4.5616 35.0716 4.9169 35.4613 5.40974C35.8625 5.89112 36.1662 6.51003 36.3725 7.26648C36.5788 8.02292 36.6819 8.9341 36.6819 10C36.6819 11.0659 36.5788 11.9771 36.3725 12.7335C36.1662 13.49 35.8625 14.1146 35.4613 14.6075C35.0716 15.0888 34.5903 15.4441 34.0172 15.6734C33.4556 15.8911 32.8138 16 32.0917 16H28V4ZM32.0917 14.1261C32.8252 14.1261 33.3926 13.9026 33.7937 13.4556C34.1948 12.9971 34.3954 12.3152 34.3954 11.4097V8.59026C34.3954 7.68481 34.1948 7.0086 33.7937 6.5616C33.3926 6.10315 32.8252 5.87392 32.0917 5.87392H30.149V14.1261H32.0917Z"
fill="currentColor"
/>
</svg>
)
}
export function NovelTokens() {
const frame = useCurrentFrame()
const k = Math.round(
K *
interpolate(frame, [18, 92], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
}),
)
const zoom = interpolate(frame, [0, 150], [1.06, 1.12], {
extrapolateRight: "clamp",
easing: Easing.inOut(Easing.quad),
})
return (
<AbsoluteFill style={{ background: "#0c0c0c", overflow: "hidden" }}>
<Img
src={staticFile("book.jpg")}
style={{
position: "absolute",
width: "100%",
height: "100%",
objectFit: "cover",
objectPosition: "center 34%",
transform: `scale(${zoom})`,
transformOrigin: "center 30%",
}}
/>
{/* legibility scrims */}
<div
style={{
position: "absolute",
inset: 0,
background:
"linear-gradient(to bottom, rgba(0,0,0,0.55) 0%, rgba(0,0,0,0) 20%, rgba(0,0,0,0.1) 44%, rgba(0,0,0,0.86) 100%)",
}}
/>
<div
style={{
position: "absolute",
inset: 0,
boxSizing: "border-box",
padding: 64,
color: c.white,
fontFamily: MONO,
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
}}
>
{/* header */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<DataWordmark height={30} />
<div style={{ fontSize: 20, fontWeight: 500, color: c.dim, letterSpacing: 1 }}>JUN 2228, 2026</div>
</div>
{/* bottom block */}
<div>
<div style={{ fontSize: 23, fontWeight: 600, color: c.dim, letterSpacing: 2, marginBottom: 8 }}>
OPENCODE GO · LAST WEEK
</div>
<div
style={{
fontSize: 168,
fontWeight: 600,
lineHeight: 0.92,
letterSpacing: -5,
fontVariantNumeric: "tabular-nums",
}}
>
{k}K
</div>
<div style={{ fontSize: 50, fontWeight: 600, letterSpacing: -1, marginTop: 4 }}>tokens per request</div>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginTop: 30,
fontSize: 20,
fontWeight: 500,
}}
>
<div style={{ color: c.dim }}>{nf.format(AVG)} tokens / request · last week</div>
<div style={{ color: c.white }}>opencode.ai/data</div>
</div>
</div>
</div>
</AbsoluteFill>
)
}
+139
View File
@@ -0,0 +1,139 @@
import React from "react"
import { AbsoluteFill, Easing, Img, interpolate, staticFile, useCurrentFrame } from "remotion"
const c = {
white: "#ffffff",
dim: "rgba(255,255,255,0.72)",
faint: "rgba(255,255,255,0.55)",
}
const MONO = '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
// verified: NZ OpenCode Go, week of Jun 22-28, 2026 (2026-W26)
const TOKENS = 40_915_594_381 // 40.9B
const SHEEP = 23_600_000 // 23.6M
const PER_SHEEP = Math.round(TOKENS / SHEEP) // 1,734
const nf = new Intl.NumberFormat("en-US")
// the correct opencode "DATA" wordmark (white, over photo)
function DataWordmark({ height = 30 }: { height?: number }) {
return (
<svg width={(height * 66) / 20} height={height} viewBox="0 0 66 20" fill="none" style={{ color: c.white }}>
<path opacity="0.35" d="M12 16H4V8H12V16Z" fill="currentColor" />
<path d="M12 4H4V16H12V4ZM16 20H0V0H16V20Z" fill="currentColor" />
<path
d="M63.3543 16L62.5119 12.8711H58.6437L57.8013 16H55.7383L59.2454 4H61.9618L65.4689 16H63.3543ZM61.0678 7.851L60.6896 5.94269H60.4489L60.0707 7.851L59.1595 11.1347H61.9962L61.0678 7.851Z"
fill="currentColor"
/>
<path d="M52.5951 5.87392V16H50.4461V5.87392H47.4375V4H55.6209V5.87392H52.5951Z" fill="currentColor" />
<path
d="M45.2059 16L44.3635 12.8711H40.4953L39.6529 16H37.5898L41.097 4H43.8133L47.3205 16H45.2059ZM42.9194 7.851L42.5411 5.94269H42.3004L41.9222 7.851L41.011 11.1347H43.8477L42.9194 7.851Z"
fill="currentColor"
/>
<path
d="M28 4H32.0917C32.8138 4 33.4556 4.11461 34.0172 4.34384C34.5903 4.5616 35.0716 4.9169 35.4613 5.40974C35.8625 5.89112 36.1662 6.51003 36.3725 7.26648C36.5788 8.02292 36.6819 8.9341 36.6819 10C36.6819 11.0659 36.5788 11.9771 36.3725 12.7335C36.1662 13.49 35.8625 14.1146 35.4613 14.6075C35.0716 15.0888 34.5903 15.4441 34.0172 15.6734C33.4556 15.8911 32.8138 16 32.0917 16H28V4ZM32.0917 14.1261C32.8252 14.1261 33.3926 13.9026 33.7937 13.4556C34.1948 12.9971 34.3954 12.3152 34.3954 11.4097V8.59026C34.3954 7.68481 34.1948 7.0086 33.7937 6.5616C33.3926 6.10315 32.8252 5.87392 32.0917 5.87392H30.149V14.1261H32.0917Z"
fill="currentColor"
/>
</svg>
)
}
export function NZSheep() {
const frame = useCurrentFrame()
const count = Math.round(
PER_SHEEP *
interpolate(frame, [18, 90], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
}),
)
// slow Ken Burns push-in (scale up only — never reveals an edge)
const zoom = interpolate(frame, [0, 150], [1.06, 1.12], {
extrapolateRight: "clamp",
easing: Easing.inOut(Easing.quad),
})
return (
<AbsoluteFill style={{ background: "#0c0c0c", overflow: "hidden" }}>
{/* the sheep, staring */}
<Img
src={staticFile("sheep.jpg")}
style={{
position: "absolute",
width: "100%",
height: "100%",
objectFit: "cover",
objectPosition: "center 38%",
transform: `scale(${zoom})`,
transformOrigin: "center 35%",
}}
/>
{/* legibility scrims */}
<div
style={{
position: "absolute",
inset: 0,
background:
"linear-gradient(to bottom, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0) 22%, rgba(0,0,0,0) 48%, rgba(0,0,0,0.78) 100%)",
}}
/>
{/* content */}
<div
style={{
position: "absolute",
inset: 0,
boxSizing: "border-box",
padding: 64,
color: c.white,
fontFamily: MONO,
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
}}
>
{/* header */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<DataWordmark height={30} />
<div style={{ fontSize: 20, fontWeight: 500, color: c.dim, letterSpacing: 1 }}>JUN 2228, 2026</div>
</div>
{/* bottom block */}
<div>
<div style={{ fontSize: 23, fontWeight: 600, color: c.dim, letterSpacing: 2, marginBottom: 8 }}>
OPENCODE GO · NEW ZEALAND
</div>
<div
style={{
fontSize: 168,
fontWeight: 600,
lineHeight: 0.92,
letterSpacing: -5,
fontVariantNumeric: "tabular-nums",
}}
>
{nf.format(count)}
</div>
<div style={{ fontSize: 50, fontWeight: 600, letterSpacing: -1, marginTop: 4 }}>tokens per sheep</div>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginTop: 34,
fontSize: 20,
fontWeight: 500,
}}
>
<div style={{ color: c.dim }}>40.9B tokens ÷ 23.6M sheep · last week</div>
<div style={{ color: c.white }}>opencode.ai/data</div>
</div>
</div>
</div>
</AbsoluteFill>
)
}
+254
View File
@@ -0,0 +1,254 @@
import React from "react"
import { AbsoluteFill, Easing, interpolate, useCurrentFrame, useVideoConfig } from "remotion"
import { days, launchIndex, glmWeekTokensT, segments } from "./data"
// stats.opencode.ai design tokens (light theme)
const c = {
bg: "#ffffff",
ink: "#161616",
muted: "#5c5c5c",
faint: "#808080",
line: "#e6e6e6",
dot: "#ededed",
accent: "#3b5cf6",
accentHi: "#5b78ff",
}
const MONO = '"IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
const W = 1080
const DOT_MASK =
"url(\"data:image/svg+xml,%3Csvg viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0H2V2H0V0Z' fill='black'/%3E%3C/svg%3E\")"
const field = segments.filter((s) => !s.hero)
const glmColor = segments.find((s) => s.hero)!.color
const clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v))
// the correct opencode "DATA" wordmark (from stats.opencode.ai header)
function DataWordmark({ height = 30, color = c.ink }: { height?: number; color?: string }) {
return (
<svg width={(height * 66) / 20} height={height} viewBox="0 0 66 20" fill="none" style={{ color }}>
<path opacity="0.2" d="M12 16H4V8H12V16Z" fill="currentColor" />
<path d="M12 4H4V16H12V4ZM16 20H0V0H16V20Z" fill="currentColor" />
<path
d="M63.3543 16L62.5119 12.8711H58.6437L57.8013 16H55.7383L59.2454 4H61.9618L65.4689 16H63.3543ZM61.0678 7.851L60.6896 5.94269H60.4489L60.0707 7.851L59.1595 11.1347H61.9962L61.0678 7.851Z"
fill="currentColor"
/>
<path d="M52.5951 5.87392V16H50.4461V5.87392H47.4375V4H55.6209V5.87392H52.5951Z" fill="currentColor" />
<path
d="M45.2059 16L44.3635 12.8711H40.4953L39.6529 16H37.5898L41.097 4H43.8133L47.3205 16H45.2059ZM42.9194 7.851L42.5411 5.94269H42.3004L41.9222 7.851L41.011 11.1347H43.8477L42.9194 7.851Z"
fill="currentColor"
/>
<path
d="M28 4H32.0917C32.8138 4 33.4556 4.11461 34.0172 4.34384C34.5903 4.5616 35.0716 4.9169 35.4613 5.40974C35.8625 5.89112 36.1662 6.51003 36.3725 7.26648C36.5788 8.02292 36.6819 8.9341 36.6819 10C36.6819 11.0659 36.5788 11.9771 36.3725 12.7335C36.1662 13.49 35.8625 14.1146 35.4613 14.6075C35.0716 15.0888 34.5903 15.4441 34.0172 15.6734C33.4556 15.8911 32.8138 16 32.0917 16H28V4ZM32.0917 14.1261C32.8252 14.1261 33.3926 13.9026 33.7937 13.4556C34.1948 12.9971 34.3954 12.3152 34.3954 11.4097V8.59026C34.3954 7.68481 34.1948 7.0086 33.7937 6.5616C33.3926 6.10315 32.8252 5.87392 32.0917 5.87392H30.149V14.1261H32.0917Z"
fill="currentColor"
/>
</svg>
)
}
export function GLM52Rise() {
const frame = useCurrentFrame()
const { fps } = useVideoConfig()
// ---------- virtual camera ----------
// open zoomed-in on the field, pan right while the blue fills, then pull back to reveal.
const K = [0, 32, 150, 206, 240]
const ease = Easing.inOut(Easing.cubic)
const opt = { extrapolateLeft: "clamp" as const, extrapolateRight: "clamp" as const, easing: ease }
const s = interpolate(frame, K, [1.82, 1.72, 1.72, 1.0, 1.0], opt)
let fx = interpolate(frame, K, [420, 438, 760, 540, 540], opt)
let fy = interpolate(frame, K, [664, 664, 664, 540, 540], opt)
// keep the framing inside the 1080 canvas so edges never reveal black
fx = clamp(fx, 540 / s, W - 540 / s)
fy = clamp(fy, 540 / s, W - 540 / s)
const camera = `translate(${540 - fx * s}px, ${540 - fy * s}px) scale(${s})`
// ---------- blue sweep (synced to the pan) ----------
const p = interpolate(frame, [32, 150], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: ease })
const revealAmount = p * (days.length - launchIndex) + 0.35
const fillOf = (i: number) => clamp(revealAmount - (i - launchIndex), 0, 1)
// token number climbs as the camera pulls back and the headline re-enters frame
const tokensT =
glmWeekTokensT *
interpolate(frame, [150, 202], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
})
// chart geometry
const chartH = 440
const chartW = 936
const gap = 14
const EXAGGERATE = 2.876 // broken y-axis: GLM-5.2 magnified ~2.9x for emphasis
return (
<AbsoluteFill style={{ background: c.bg, overflow: "hidden" }}>
<div
style={{
position: "absolute",
width: W,
height: W,
background: c.bg,
transformOrigin: "0 0",
transform: camera,
}}
>
<div
style={{
width: W,
height: W,
boxSizing: "border-box",
padding: 72,
color: c.ink,
fontFamily: MONO,
display: "flex",
flexDirection: "column",
}}
>
{/* header */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<DataWordmark height={30} />
<div style={{ fontSize: 20, fontWeight: 500, color: c.faint, letterSpacing: 1 }}>JUN 1225, 2026</div>
</div>
{/* headline (static) */}
<div style={{ marginTop: 52 }}>
<div style={{ fontSize: 92, fontWeight: 600, lineHeight: 0.98, letterSpacing: -2 }}>
GLM-5.2 <span style={{ color: c.accent }}>broke out</span>
</div>
<div style={{ marginTop: 22, fontSize: 30, fontWeight: 500, color: c.muted }}>
From 0 to <span style={{ color: c.ink }}>{tokensT.toFixed(2)}T tokens</span> in a week.
</div>
</div>
{/* stacked chart */}
<div style={{ flex: 1, display: "flex", alignItems: "flex-end", marginTop: 40 }}>
<div
style={{
position: "relative",
width: chartW,
height: chartH,
margin: "0 auto",
borderBottom: `2px solid ${c.ink}`,
boxSizing: "border-box",
}}
>
{/* faint dotted backdrop */}
<div
style={{
position: "absolute",
left: -8,
right: -8,
top: -8,
bottom: 0,
background: c.dot,
WebkitMaskImage: DOT_MASK,
maskImage: DOT_MASK,
WebkitMaskSize: "12px 12px",
maskSize: "12px 12px",
WebkitMaskRepeat: "repeat",
maskRepeat: "repeat",
}}
/>
{/* columns */}
<div style={{ position: "absolute", inset: 0, display: "flex", gap, alignItems: "flex-end" }}>
{days.map((d, i) => {
const glmShare = d.glm / d.total
const blueH = Math.round(glmShare * EXAGGERATE * chartH)
const filled = Math.round(blueH * fillOf(i))
const slotGap = filled > 2 ? 5 : 0
const fieldH = chartH - filled - slotGap
const fieldTotal = d.dsf + d.dsp + d.mm + d.others
return (
<div key={i} style={{ position: "relative", flex: 1, height: chartH }}>
{/* gray field of other models (already in place) */}
<div
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
height: fieldH,
display: "flex",
flexDirection: "column-reverse",
}}
>
{field.map((seg) => (
<div
key={seg.key}
style={{
height: Math.round(((d[seg.key as keyof typeof d] as number) / fieldTotal) * fieldH),
background: seg.color,
borderTop: `2px solid ${c.bg}`,
}}
/>
))}
</div>
{/* GLM-5.2, animates in */}
{filled > 2 && (
<div
style={{
position: "absolute",
left: 0,
right: 0,
bottom: 0,
height: filled,
background: glmColor,
borderTop: `2px solid ${c.accentHi}`,
}}
/>
)}
</div>
)
})}
</div>
</div>
</div>
{/* day axis */}
<div style={{ display: "flex", gap, width: chartW, margin: "14px auto 0" }}>
{days.map((d, i) => (
<div
key={i}
style={{
flex: 1,
textAlign: "center",
fontSize: 15,
fontWeight: 500,
color: i === days.length - 1 ? c.ink : c.faint,
}}
>
{i === 0 || i === launchIndex || i === days.length - 1 ? d.date : ""}
</div>
))}
</div>
{/* footer */}
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginTop: 28,
paddingTop: 22,
borderTop: `1px solid ${c.line}`,
fontSize: 20,
fontWeight: 500,
}}
>
<div style={{ display: "inline-flex", alignItems: "center", gap: 9, color: c.muted }}>
<span style={{ width: 13, height: 13, background: c.accent, display: "inline-block" }} />
GLM-5.2
</div>
<div style={{ color: c.ink }}>opencode.ai/data</div>
</div>
</div>
</div>
</AbsoluteFill>
)
}
+10
View File
@@ -0,0 +1,10 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
/* biome-ignore-all lint: auto-generated */
/// <reference path="../../sst-env.d.ts" />
import "sst"
export {}
+160 -77
View File
@@ -24,19 +24,21 @@
"prettier": "3.6.2",
"semver": "^7.6.0",
"sst": "catalog:",
"turbo": "2.8.13",
"turbo": "2.10.2",
},
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.17.11",
"version": "1.17.18",
"dependencies": {
"@corvu/drawer": "catalog:",
"@dnd-kit/abstract": "0.5.0",
"@dnd-kit/dom": "0.5.0",
"@dnd-kit/helpers": "0.5.0",
"@dnd-kit/solid": "0.5.0",
"@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*",
"@opencode-ai/schema": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/session-ui": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -71,6 +73,7 @@
"shiki": "catalog:",
"solid-js": "catalog:",
"solid-list": "catalog:",
"solid-presence": "0.2.0",
"tailwindcss": "catalog:",
},
"devDependencies": {
@@ -92,7 +95,7 @@
},
"packages/cli": {
"name": "@opencode-ai/cli",
"version": "1.17.11",
"version": "1.17.18",
"bin": {
"lildax": "./bin/lildax.cjs",
},
@@ -138,9 +141,23 @@
"effect",
],
},
"packages/codemode": {
"name": "@opencode-ai/codemode",
"version": "1.17.18",
"dependencies": {
"acorn": "8.15.0",
"effect": "catalog:",
"typescript": "catalog:",
},
"devDependencies": {
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
},
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.17.11",
"version": "1.17.18",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -176,7 +193,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.17.11",
"version": "1.17.18",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -203,7 +220,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.17.11",
"version": "1.17.18",
"dependencies": {
"@ai-sdk/anthropic": "3.0.82",
"@ai-sdk/openai": "3.0.48",
@@ -225,7 +242,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.17.11",
"version": "1.17.18",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -249,7 +266,7 @@
},
"packages/console/support": {
"name": "@opencode-ai/console-support",
"version": "1.17.11",
"version": "1.17.18",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@opencode-ai/console-core": "workspace:*",
@@ -269,7 +286,7 @@
},
"packages/core": {
"name": "@opencode-ai/core",
"version": "1.17.11",
"version": "1.17.18",
"bin": {
"opencode": "./bin/opencode",
},
@@ -293,7 +310,7 @@
"@ai-sdk/provider-utils": "4.0.23",
"@ai-sdk/togetherai": "2.0.41",
"@ai-sdk/vercel": "2.0.39",
"@ai-sdk/xai": "3.0.82",
"@ai-sdk/xai": "3.0.102",
"@aws-sdk/credential-providers": "3.1057.0",
"@effect/opentelemetry": "catalog:",
"@effect/platform-node": "catalog:",
@@ -321,7 +338,7 @@
"drizzle-orm": "catalog:",
"effect": "catalog:",
"fuzzysort": "3.1.0",
"gitlab-ai-provider": "6.9.3",
"gitlab-ai-provider": "6.10.0",
"glob": "13.0.5",
"google-auth-library": "10.5.0",
"gray-matter": "4.0.3",
@@ -334,7 +351,7 @@
"npm-package-arg": "13.0.2",
"semver": "^7.6.3",
"turndown": "7.2.0",
"venice-ai-sdk-provider": "2.0.2",
"venice-ai-sdk-provider": "2.1.1",
"which": "6.0.1",
"xdg-basedir": "5.1.0",
"zod": "catalog:",
@@ -363,7 +380,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.17.11",
"version": "1.17.18",
"dependencies": {
"@zip.js/zip.js": "2.7.62",
"effect": "catalog:",
@@ -417,7 +434,7 @@
},
"packages/effect-drizzle-sqlite": {
"name": "@opencode-ai/effect-drizzle-sqlite",
"version": "1.17.11",
"version": "1.17.18",
"dependencies": {
"drizzle-orm": "catalog:",
"effect": "catalog:",
@@ -431,7 +448,7 @@
},
"packages/effect-sqlite-node": {
"name": "@opencode-ai/effect-sqlite-node",
"version": "1.17.11",
"version": "1.17.18",
"dependencies": {
"effect": "catalog:",
},
@@ -443,7 +460,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.17.11",
"version": "1.17.18",
"dependencies": {
"@hono/standard-validator": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -475,7 +492,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.17.11",
"version": "1.17.18",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -491,7 +508,7 @@
},
"packages/http-recorder": {
"name": "@opencode-ai/http-recorder",
"version": "1.17.11",
"version": "1.17.18",
"dependencies": {
"@effect/platform-node": "4.0.0-beta.83",
"@effect/platform-node-shared": "4.0.0-beta.83",
@@ -522,7 +539,7 @@
},
"packages/llm": {
"name": "@opencode-ai/llm",
"version": "1.17.11",
"version": "1.17.18",
"dependencies": {
"@opencode-ai/schema": "workspace:*",
"@smithy/eventstream-codec": "4.2.14",
@@ -541,7 +558,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.17.11",
"version": "1.17.18",
"bin": {
"opencode": "./bin/opencode",
},
@@ -553,7 +570,7 @@
"@ai-sdk/amazon-bedrock": "4.0.112",
"@ai-sdk/anthropic": "3.0.82",
"@ai-sdk/azure": "3.0.49",
"@ai-sdk/cerebras": "2.0.41",
"@ai-sdk/cerebras": "2.0.60",
"@ai-sdk/cohere": "3.0.27",
"@ai-sdk/deepinfra": "2.0.41",
"@ai-sdk/gateway": "3.0.104",
@@ -567,7 +584,7 @@
"@ai-sdk/provider": "3.0.8",
"@ai-sdk/togetherai": "2.0.41",
"@ai-sdk/vercel": "2.0.39",
"@ai-sdk/xai": "3.0.82",
"@ai-sdk/xai": "3.0.102",
"@aws-sdk/credential-providers": "3.1057.0",
"@clack/prompts": "1.0.0-alpha.1",
"@effect/opentelemetry": "catalog:",
@@ -578,6 +595,7 @@
"@octokit/graphql": "9.0.2",
"@octokit/rest": "catalog:",
"@openauthjs/openauth": "catalog:",
"@opencode-ai/codemode": "workspace:*",
"@opencode-ai/llm": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/protocol": "workspace:*",
@@ -613,7 +631,7 @@
"drizzle-orm": "catalog:",
"effect": "catalog:",
"fuzzysort": "3.1.0",
"gitlab-ai-provider": "6.9.3",
"gitlab-ai-provider": "6.10.0",
"glob": "13.0.5",
"google-auth-library": "10.5.0",
"gray-matter": "4.0.3",
@@ -637,7 +655,7 @@
"tree-sitter-powershell": "0.25.10",
"turndown": "7.2.0",
"ulid": "catalog:",
"venice-ai-sdk-provider": "2.0.2",
"venice-ai-sdk-provider": "2.1.1",
"vscode-jsonrpc": "8.2.1",
"web-tree-sitter": "0.25.10",
"ws": "8.21.0",
@@ -671,7 +689,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.17.11",
"version": "1.17.18",
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@opencode-ai/sdk": "workspace:*",
@@ -688,9 +706,9 @@
"typescript": "catalog:",
},
"peerDependencies": {
"@opentui/core": ">=0.3.4",
"@opentui/keymap": ">=0.3.4",
"@opentui/solid": ">=0.3.4",
"@opentui/core": ">=0.4.3",
"@opentui/keymap": ">=0.4.3",
"@opentui/solid": ">=0.4.3",
},
"optionalPeers": [
"@opentui/core",
@@ -747,7 +765,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.17.11",
"version": "1.17.18",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -762,7 +780,7 @@
},
"packages/server": {
"name": "@opencode-ai/server",
"version": "1.17.11",
"version": "1.17.18",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/protocol": "workspace:*",
@@ -777,7 +795,7 @@
},
"packages/session-ui": {
"name": "@opencode-ai/session-ui",
"version": "1.17.11",
"version": "1.17.18",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -821,7 +839,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.17.11",
"version": "1.17.18",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -834,7 +852,7 @@
},
"packages/stats/app": {
"name": "@opencode-ai/stats-app",
"version": "1.17.11",
"version": "1.17.18",
"dependencies": {
"@ibm/plex": "6.4.1",
"@opencode-ai/stats-core": "workspace:*",
@@ -867,7 +885,7 @@
},
"packages/stats/core": {
"name": "@opencode-ai/stats-core",
"version": "1.17.11",
"version": "1.17.18",
"dependencies": {
"@aws-sdk/client-athena": "3.933.0",
"@planetscale/database": "1.19.0",
@@ -886,7 +904,7 @@
},
"packages/stats/server": {
"name": "@opencode-ai/stats-server",
"version": "1.17.11",
"version": "1.17.18",
"dependencies": {
"@aws-sdk/client-firehose": "3.933.0",
"@effect/platform-node": "catalog:",
@@ -927,7 +945,7 @@
},
"packages/tui": {
"name": "@opencode-ai/tui",
"version": "1.17.11",
"version": "1.17.18",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
@@ -954,7 +972,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.17.11",
"version": "1.17.18",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/diffs": "catalog:",
@@ -1005,7 +1023,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.17.11",
"version": "1.17.18",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -1048,6 +1066,7 @@
],
"patchedDependencies": {
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
"@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
@@ -1057,7 +1076,6 @@
"@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch",
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
"@ai-sdk/xai@3.0.82": "patches/@ai-sdk%2Fxai@3.0.82.patch",
"@tanstack/virtual-core@3.17.0": "patches/@tanstack%2Fvirtual-core@3.17.0.patch",
"pacote@21.5.0": "patches/pacote@21.5.0.patch",
},
@@ -1070,6 +1088,7 @@
},
"catalog": {
"@cloudflare/workers-types": "4.20251008.0",
"@corvu/drawer": "0.2.4",
"@effect/opentelemetry": "4.0.0-beta.83",
"@effect/platform-node": "4.0.0-beta.83",
"@effect/sql-sqlite-bun": "4.0.0-beta.83",
@@ -1080,9 +1099,9 @@
"@npmcli/arborist": "9.4.0",
"@octokit/rest": "22.0.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@opentui/core": "0.3.4",
"@opentui/keymap": "0.3.4",
"@opentui/solid": "0.3.4",
"@opentui/core": "0.4.3",
"@opentui/keymap": "0.4.3",
"@opentui/solid": "0.4.3",
"@pierre/diffs": "1.2.10",
"@playwright/test": "1.59.1",
"@sentry/solid": "10.36.0",
@@ -1195,7 +1214,7 @@
"@ai-sdk/vercel": ["@ai-sdk/vercel@2.0.39", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8eu3ljJpkCTP4ppcyYB+NcBrkcBoSOFthCSgk5VnjaxnDaOJFaxnPwfddM7wx3RwMk2CiK1O61Px/LlqNc7QkQ=="],
"@ai-sdk/xai": ["@ai-sdk/xai@3.0.82", "", { "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-A0VFMufnVf4wODcT3SPQUUzvYXiIO1VhFuXj9r6z/vP4rlo+QRDPw3WSTchcz93ROQWSfBE3I6Szqz342OHi5w=="],
"@ai-sdk/xai": ["@ai-sdk/xai@3.0.102", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.56", "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.35" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-NeQyOR7OCqDMgaLS4uNX/ep/HrwUzzFYLzXQSRoqLy2jsnqxAJhsgltRwAwf+ADjyPBIAKEOestWnIQA+LrLrQ=="],
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
@@ -1451,6 +1470,10 @@
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20251008.0", "", {}, "sha512-dZLkO4PbCL0qcCSKzuW7KE4GYe49lI12LCfQ5y9XeSwgYBoAUbwH4gmJ6A0qUIURiTJTkGkRkhVPqpq2XNgYRA=="],
"@corvu/dialog": ["@corvu/dialog@0.2.4", "", { "dependencies": { "@corvu/utils": "~0.4.2", "solid-dismissible": "~0.1.1", "solid-focus-trap": "~0.1.8", "solid-presence": "~0.2.0", "solid-prevent-scroll": "~0.1.10" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-n54vJq+fOy8GVrnYBdJpD6JXNuyx7LOeMrRxwzAvZnYGpW8+AA12tnb/P/2emJj/HjOO5otheGKb0breshdFlA=="],
"@corvu/drawer": ["@corvu/drawer@0.2.4", "", { "dependencies": { "@corvu/dialog": "~0.2.4", "@corvu/utils": "~0.4.2", "@solid-primitives/memo": "^1.4.1", "solid-transition-size": "~0.1.4" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-7jQoGZ8ROB9CmXam2nMY2wEskU3IoFwZQywkF/7vrBc/edGsPv7mOVQ1GN6G+4nd7nrMZ3UtqHAhmRh9V0azlw=="],
"@corvu/utils": ["@corvu/utils@0.4.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.11" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-Ox2kYyxy7NoXdKWdHeDEjZxClwzO4SKM8plAaVwmAJPxHMqA0rLOoAsa+hBDwRLpctf+ZRnAd/ykguuJidnaTA=="],
"@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
@@ -1923,6 +1946,8 @@
"@opencode-ai/client": ["@opencode-ai/client@workspace:packages/client"],
"@opencode-ai/codemode": ["@opencode-ai/codemode@workspace:packages/codemode"],
"@opencode-ai/console-app": ["@opencode-ai/console-app@workspace:packages/console/app"],
"@opencode-ai/console-core": ["@opencode-ai/console-core@workspace:packages/console/core"],
@@ -2013,27 +2038,27 @@
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="],
"@opentui/core": ["@opentui/core@0.3.4", "", { "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.3.4", "@opentui/core-darwin-x64": "0.3.4", "@opentui/core-linux-arm64": "0.3.4", "@opentui/core-linux-arm64-musl": "0.3.4", "@opentui/core-linux-x64": "0.3.4", "@opentui/core-linux-x64-musl": "0.3.4", "@opentui/core-win32-arm64": "0.3.4", "@opentui/core-win32-x64": "0.3.4" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-y0DlrChP9lcJ4jC5z/1wMS34+ygfSTW7gD5OJHwJaAScfmlFvuJOZbwmCGrJURZ+5wFBxuOi9LatZsmeAUIKAA=="],
"@opentui/core": ["@opentui/core@0.4.3", "", { "dependencies": { "bun-ffi-structs": "0.2.4", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.3", "@opentui/core-darwin-x64": "0.4.3", "@opentui/core-linux-arm64": "0.4.3", "@opentui/core-linux-arm64-musl": "0.4.3", "@opentui/core-linux-x64": "0.4.3", "@opentui/core-linux-x64-musl": "0.4.3", "@opentui/core-win32-arm64": "0.4.3", "@opentui/core-win32-x64": "0.4.3" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-rrJfAk13tALDqldYjhc78eWQ+aKq1iknJgffIOg3OwyZoqQo+p6gtuqyhmWvXIfQzlNUbpgpCPcxbXlhMnlaHQ=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.3.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4A7JYXUsZqhu9PPCe07E30ourSJYkitkwMujUyNKjM5e/dHNDVnz+5r5cO3M5snofLafc1DN7+9jEPn4UQzchQ=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.4.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p5+7AAxpxGuDGagyQfewKtmTFnN7THvTVY4FyKqUtJomNaHdQXPHztapNNzMx0DGWbwOUbVKzpL+yc3CZY3chQ=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.3.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-Jvm9E8n2sPhKEyKSXn9GlmJcj8WoJXJTooXb3djwjVaiimjihIj0XxHzCWhdqbDtQp+VxDFyCKoQagOOz20qhA=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.4.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-+fh0vEUE0lwVC7RW5ijYLRlTLp5NfvCRj8SzxDVd7IL2j2ssB6YXcfIbXq2EW7UGnrejwPRXf1tgUrIXW9KmOw=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-0uPuHCeZxm/O7+L+iNQl8zRAfehiwYstKkT9J0uTZO64/byBCLvy5lvn1DiE/72s/nTJ5nwpLN+pQs2/WYVKLQ=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-gl6qA5QJy6u8Cbt7gOtHbhhfMZ4qQDb0kEwFXHcMGmbnKzz4OHoq74D6tNjyvSQB9saoC7C6C0tvn2DcJOuNog=="],
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-sJYUzYcSOb5PCXRlhwsse/fdsMiVomNvIwq/2TDhAANef+YPO3Br+OH9kQRbuj0bjVDmUS36SGYWSTFu2lUO+A=="],
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-8p8g8/AEq/xFGpQ7XcIFKcAqjc0QwsZcv+Ll9RbCDpUA56FGH6jfLDir0KYTNTgYXJTIrBIENI9K46VuxMUMQA=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-btYIQeNdPbN4JCrCjVB/RwMGrnRY7qWB2piNEfALSByuULKNjPKQ33PYIj38Yd01zCvCV7FotIeXEGSHx3tgCA=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.4.3", "", { "os": "linux", "cpu": "x64" }, "sha512-dXpJitiZdYE3hq2Pvx6e9I0uPQSOcnaLLp1pDgWAHv+3kvKSHEX//9Yr/pV/Ua6qqT7p+2D/K4vXNap/NKVo2w=="],
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-fhmUey4oJJ2+N62xlIgAPxAl36Fa7wYffqDOT4QLpm0jfyD5xzo+wL/hr2zUqaEI439R8Iq6jHNxf/Nsx1WuuQ=="],
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.4.3", "", { "os": "linux", "cpu": "x64" }, "sha512-/QiFpCrpU2O7vy8QYmLIQYbvAtKDgmqcVjR7dGtqSzkiQk3ktNJoo5RozG7ueXnjung1Wp0nKldKxo2Csg/OrA=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.3.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-sh432vPU+eLp8eA4I0KWKKn7D0VHbk01YTg6mA9/ihCNYHntc6LZ8/sLvsPv8CvKscMotfIkh3M5YhdS36BuXw=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.4.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-Mx2zuOjrhm/z2SDS6RExIyjP/SnN/8QhhagxURUw0jQi/NssGSeAllu1cBAFFnhobJL5QLTE4FU4CRhUK9svgg=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.3.4", "", { "os": "win32", "cpu": "x64" }, "sha512-dw8FcjUZaLAjw25P3/7BarobCh/QOHn3srYaWYQdysoqyvSlPkQumpI8kV/KgpJtdITU1GW02MQC4EeLIFFalA=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.4.3", "", { "os": "win32", "cpu": "x64" }, "sha512-NuoqvWKGXaYnmlqvu7Gg2lLI6yVMnS9OfWBvxp+7Q+McSgHFSTQmYBXaPpvQ8HikpQXE1nCeMPtuSG4PdZHe2w=="],
"@opentui/keymap": ["@opentui/keymap@0.3.4", "", { "dependencies": { "@opentui/core": "0.3.4" }, "peerDependencies": { "@opentui/react": "0.3.4", "@opentui/solid": "0.3.4", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-8fo6BZWQgCjANfbKkzPo0ghAzS1E7TlHjDDS+SUhrX01qEUO1clFTRssKluHbXd2UJY1Ehle01TV5bFmY78f8w=="],
"@opentui/keymap": ["@opentui/keymap@0.4.3", "", { "dependencies": { "@opentui/core": "0.4.3" }, "peerDependencies": { "@opentui/react": "0.4.3", "@opentui/solid": "0.4.3", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-sinX0pyQBRrEvo89PSSUbSUDIYpL3xWo81VEfec58VFoVRB5FG48/deAtvRTQfJ8w1kgbzN8hzdOXdSm61zBmw=="],
"@opentui/solid": ["@opentui/solid@0.3.4", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.3.4", "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-gin1VnsVBahX0nrU3mpgh5U1qvyJBIZu4NE5mc0YnObWOEf9HVNxKY4/BpUvQPh91kT6zeOzTBvAvYK4R7g9MQ=="],
"@opentui/solid": ["@opentui/solid@0.4.3", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.4.3", "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-RcV0+S8HMdXOASyr7HmJUBuTUIaFPzAxMDa44VftS5C2JUgrmAuWo0Njv1q3TWRB1owjHnyKhEfWGKq7A82wxw=="],
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
@@ -2649,6 +2674,8 @@
"@solid-primitives/media": ["@solid-primitives/media@2.3.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-hQ4hLOGvfbugQi5Eu1BFWAIJGIAzztq9x0h02xgBGl2l0Jaa3h7tg6bz5tV1NSuNYVGio4rPoa7zVQQLkkx9dA=="],
"@solid-primitives/memo": ["@solid-primitives/memo@1.5.0", "", { "dependencies": { "@solid-primitives/scheduled": "^1.5.3", "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-nSMHpFdnOMP88t7lqtktUXJlhQdJk0BQs2v3jUqcn+OtFwUm27Oa/coKl1BHDxL/65jR5drjqxxvuhaIpiUi0w=="],
"@solid-primitives/props": ["@solid-primitives/props@3.2.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-XzG6en9gSFwmvbKcATm2BxL63HegZ+BAG5fmHi8jyBppQHcaths7ffz+6vYvwYy3nlgLa20ufJLj7tst+PcHFA=="],
"@solid-primitives/refs": ["@solid-primitives/refs@1.1.3", "", { "dependencies": { "@solid-primitives/utils": "^6.4.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-aam02fjNKpBteewF/UliPSQCVJsIIGOLEWQOh+ll6R/QePzBOOBMcC4G+5jTaO75JuUS1d/14Q1YXT3X0Ow6iA=="],
@@ -2773,6 +2800,18 @@
"@tufjs/models": ["@tufjs/models@4.1.0", "", { "dependencies": { "@tufjs/canonical-json": "2.0.0", "minimatch": "^10.1.1" } }, "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww=="],
"@turbo/darwin-64": ["@turbo/darwin-64@2.10.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-wBM3ObqOWnKUDmg7QfUFDkDHPFUAJmrYlYqmEM8jMPAPA/I6wRJIbWimeQUqhOiQ8xPKhzyWM+xaiUP0wz8FEQ=="],
"@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.10.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/Cq0joWnuMjDPfhjbFP4sv+C/7gkQ415zlaO4XUzD5EZxbtrKgXKvuuydMvogG8GeUnN1aDltW71RlmEfpjbyw=="],
"@turbo/linux-64": ["@turbo/linux-64@2.10.2", "", { "os": "linux", "cpu": "x64" }, "sha512-mMsf5IIhiKuceEXNstd25IbadjBXZ0amxzFOqliEzJX6HyeeHdBQPVSY583PWqYDyqM/FB8d5ZjkthfBSeuH3Q=="],
"@turbo/linux-arm64": ["@turbo/linux-arm64@2.10.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Wcng1i2kaKmXutmwxT9MUoYZvdaIekXAdlGr4+0TpgbhGLw7nDuEcRBFrxb5BbRoX1d1q8SpdRxLc45TvDZIdQ=="],
"@turbo/windows-64": ["@turbo/windows-64@2.10.2", "", { "os": "win32", "cpu": "x64" }, "sha512-SsNhM7Ho7EpAdwtrJKBOic9Hso23vu6Dp0gAfLOvUFjPzurr/sGQlXZEvr6z89ne4RDOypTwz5CBDrixpMKtXw=="],
"@turbo/windows-arm64": ["@turbo/windows-arm64@2.10.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gf+S7ICAdimT/n02bOuVWKvhHnct/HYjZg3oBNIz5hZ9ZyWHbQim9J3P5Qip8WpX0ksxF7eaBVziJCuLnjhqDg=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
@@ -3009,7 +3048,7 @@
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
@@ -3213,7 +3252,7 @@
"builder-util-runtime": ["builder-util-runtime@9.7.0", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw=="],
"bun-ffi-structs": ["bun-ffi-structs@0.2.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-N/ZWtyN0piZlrXQT7TO0V+q952orYqkfhXRXM1Hcbb+R3QSiBH4vLnib187Mrs1H7pWIYECAmPeapGYDOMCl+w=="],
"bun-ffi-structs": ["bun-ffi-structs@0.2.4", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-AJzsqoVFs1KBbJbWHIYrVZLDC3NhTqqh25awRXqzoLzmBAKr5oqk6+CwuYHAekKx+VBCYVohBoKuRq40dV+TYg=="],
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
@@ -3811,7 +3850,7 @@
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
"gitlab-ai-provider": ["gitlab-ai-provider@6.9.3", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-lWo6b6es5+k9iXaDIvE9ECzyK4zfEza4+dQ5FN8SJpEuVRi3ZBCpHIOTa32QoYEDCBaiPh+tcyca86PfNodmlg=="],
"gitlab-ai-provider": ["gitlab-ai-provider@6.10.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-oWEZ06rDO6JjB7INHO882wyBAQqCZVHiDHwCs5M+VPmdDj8TzhGXcYesA2CcV5RoI5lfHLKwGp5uKFB62VWpqw=="],
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
@@ -5069,11 +5108,15 @@
"socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
"solid-dismissible": ["solid-dismissible@0.1.1", "", { "dependencies": { "@corvu/utils": "~0.4.1" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-9kcKBJIMdS+586cA1g63HYWxKh3h89leeNHbPZ1csYjuni+NvPBtNr11l0iEX2AKKEt6FHk6qNhc/gjoYAW1pA=="],
"solid-focus-trap": ["solid-focus-trap@0.1.9", "", { "dependencies": { "@corvu/utils": "~0.4.2" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-LTyNki6GUJPRLXV5uMWPkYClB07SUMubbr2EkAddiR0CJCF/I283txilMU9RURSr/P8EewMfXWu2o3aWrK7A5A=="],
"solid-js": ["solid-js@1.9.10", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew=="],
"solid-list": ["solid-list@0.3.0", "", { "dependencies": { "@corvu/utils": "~0.4.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-t4hx/F/l8Vmq+ib9HtZYl7Z9F1eKxq3eKJTXlvcm7P7yI4Z8O7QSOOEVHb/K6DD7M0RxzVRobK/BS5aSfLRwKg=="],
"solid-presence": ["solid-presence@0.1.8", "", { "dependencies": { "@corvu/utils": "~0.4.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-pWGtXUFWYYUZNbg5YpG5vkQJyOtzn2KXhxYaMx/4I+lylTLYkITOLevaCwMRN+liCVk0pqB6EayLWojNqBFECA=="],
"solid-presence": ["solid-presence@0.2.0", "", { "dependencies": { "@corvu/utils": "~0.4.2" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-YM92o+jvpzX3XGaD4rLYmq/Kc2ZVh47GSCLEufHBFQQIurvZTs8SoGJxO8BJGNDxBKdcS8F3dYhW1SDXp4BNjA=="],
"solid-prevent-scroll": ["solid-prevent-scroll@0.1.10", "", { "dependencies": { "@corvu/utils": "~0.4.1" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-KplGPX2GHiWJLZ6AXYRql4M127PdYzfwvLJJXMkO+CMb8Np4VxqDAg5S8jLdwlEuBis/ia9DKw2M8dFx5u8Mhw=="],
@@ -5081,6 +5124,8 @@
"solid-stripe": ["solid-stripe@0.8.1", "", { "peerDependencies": { "@stripe/stripe-js": ">=1.44.1 <8.0.0", "solid-js": "^1.6.0" } }, "sha512-l2SkWoe51rsvk9u1ILBRWyCHODZebChSGMR6zHYJTivTRC0XWrRnNNKs5x1PYXsaIU71KYI6ov5CZB5cOtGLWw=="],
"solid-transition-size": ["solid-transition-size@0.1.4", "", { "dependencies": { "@corvu/utils": "~0.3.2" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-ocHVnbfy23CgfaH4cEUR/AFg0Y3CEL8Oh3n9Qv8OHFJgPh+zkmERKZQfi/xH5XvxDCizg8VjPrVUhiHB1Gza8g=="],
"solid-use": ["solid-use@0.9.1", "", { "peerDependencies": { "solid-js": "^1.7" } }, "sha512-UwvXDVPlrrbj/9ewG9ys5uL2IO4jSiwys2KPzK4zsnAcmEl7iDafZWW1Mo4BSEWOmQCGK6IvpmGHo1aou8iOFw=="],
"sonic-boom": ["sonic-boom@4.2.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="],
@@ -5303,19 +5348,7 @@
"tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
"turbo": ["turbo@2.8.13", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.13", "turbo-darwin-arm64": "2.8.13", "turbo-linux-64": "2.8.13", "turbo-linux-arm64": "2.8.13", "turbo-windows-64": "2.8.13", "turbo-windows-arm64": "2.8.13" }, "bin": { "turbo": "bin/turbo" } }, "sha512-nyM99hwFB9/DHaFyKEqatdayGjsMNYsQ/XBNO6MITc7roncZetKb97MpHxWf3uiU+LB9c9HUlU3Jp2Ixei2k1A=="],
"turbo-darwin-64": ["turbo-darwin-64@2.8.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-PmOvodQNiOj77+Zwoqku70vwVjKzL34RTNxxoARjp5RU5FOj/CGiC6vcDQhNtFPUOWSAaogHF5qIka9TBhX4XA=="],
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kI+anKcLIM4L8h+NsM7mtAUpElkCOxv5LgiQVQR8BASyDFfc8Efj5kCk3cqxuxOvIqx0sLfCX7atrHQ2kwuNJQ=="],
"turbo-linux-64": ["turbo-linux-64@2.8.13", "", { "os": "linux", "cpu": "x64" }, "sha512-j29KnQhHyzdzgCykBFeBqUPS4Wj7lWMnZ8CHqytlYDap4Jy70l4RNG46pOL9+lGu6DepK2s1rE86zQfo0IOdPw=="],
"turbo-linux-arm64": ["turbo-linux-arm64@2.8.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-OEl1YocXGZDRDh28doOUn49QwNe82kXljO1HXApjU0LapkDiGpfl3jkAlPKxEkGDSYWc8MH5Ll8S16Rf5tEBYg=="],
"turbo-windows-64": ["turbo-windows-64@2.8.13", "", { "os": "win32", "cpu": "x64" }, "sha512-717bVk1+Pn2Jody7OmWludhEirEe0okoj1NpRbSm5kVZz/yNN/jfjbxWC6ilimXMz7xoMT3IDfQFJsFR3PMANA=="],
"turbo-windows-arm64": ["turbo-windows-arm64@2.8.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-R819HShLIT0Wj6zWVnIsYvSNtRNj1q9VIyaUz0P24SMcLCbQZIm1sV09F4SDbg+KCCumqD2lcaR2UViQ8SnUJA=="],
"turbo": ["turbo@2.10.2", "", { "optionalDependencies": { "@turbo/darwin-64": "2.10.2", "@turbo/darwin-arm64": "2.10.2", "@turbo/linux-64": "2.10.2", "@turbo/linux-arm64": "2.10.2", "@turbo/windows-64": "2.10.2", "@turbo/windows-arm64": "2.10.2" }, "bin": { "turbo": "bin/turbo" } }, "sha512-wTExrNrRjB8qzIcg+ZLm0A3GFNLDsWNwdS/RBXB0FPrBDyzk3i96Yx+TxWZC7a0k1SIreFB8ciUbxjmEqTH8IQ=="],
"turndown": ["turndown@7.2.0", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A=="],
@@ -5429,7 +5462,7 @@
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"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=="],
"venice-ai-sdk-provider": ["venice-ai-sdk-provider@2.1.1", "", { "dependencies": { "@ai-sdk/openai-compatible": "^2.0.51", "@ai-sdk/provider": "^3.0.10", "@ai-sdk/provider-utils": "^4.0.30" }, "peerDependencies": { "ai": "^6.0.90" } }, "sha512-w3OHkuzzKZ3r2TOxER6myBYzZJNoDqol+DUHu3NnfBN/GETnUVxecZJab0CHQQ8GZc0jjzpFymepjcLDPS4SQg=="],
"verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="],
@@ -5573,8 +5606,6 @@
"yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
"yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
"youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="],
"youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="],
@@ -5677,7 +5708,11 @@
"@ai-sdk/vercel/@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/xai/@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/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.35" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cQrN6OUn/jvsY3OdsU6Wn+ss7vp1iwIcakZKSlSRMnYqShBfyT7Qht+eqmgxs7w9ttrw6FAG6o11AiBs+iEsTA=="],
"@ai-sdk/xai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.13", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-ZPtVYt5QIJzOta1kdUiDuCx4HhFkvNPv/rvmZ2b1iXwybYjJsCnNYR4PAw4kW7rgVfDARvHXcU64efWuqNp6bw=="],
"@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bjYld/2KGPLt78kpqbya+fD4LYS7BqVQJyUjE3qAHrYB0FR2Q90BaWEVIBZaguTWXf/A8L6uG1zO1v9TxVlGWg=="],
"@astrojs/check/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
@@ -5691,6 +5726,8 @@
"@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.11", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.6", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.1", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.21.0", "smol-toml": "^1.6.0", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-hcaxX/5aC6lQgHeGh1i+aauvSwIT6cfyFjKWvExYSxUhZZBBdvCliOtu06gbQyhbe0pGJNoNmqNlQZ5zYUuIyQ=="],
"@astrojs/mdx/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"@astrojs/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"@astrojs/sitemap/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
@@ -5899,8 +5936,12 @@
"@jsx-email/doiuse-email/htmlparser2": ["htmlparser2@9.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.1.0", "entities": "^4.5.0" } }, "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ=="],
"@kobalte/core/solid-presence": ["solid-presence@0.1.8", "", { "dependencies": { "@corvu/utils": "~0.4.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-pWGtXUFWYYUZNbg5YpG5vkQJyOtzn2KXhxYaMx/4I+lylTLYkITOLevaCwMRN+liCVk0pqB6EayLWojNqBFECA=="],
"@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
"@mdx-js/mdx/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"@mdx-js/mdx/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
@@ -5983,6 +6024,8 @@
"@opencode-ai/core/@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/core/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"@opencode-ai/desktop/@actions/artifact": ["@actions/artifact@4.0.0", "", { "dependencies": { "@actions/core": "^1.10.0", "@actions/github": "^6.0.1", "@actions/http-client": "^2.1.0", "@azure/core-http": "^3.0.5", "@azure/storage-blob": "^12.15.0", "@octokit/core": "^5.2.1", "@octokit/plugin-request-log": "^1.0.4", "@octokit/plugin-retry": "^3.0.9", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@protobuf-ts/plugin": "^2.2.3-alpha.1", "archiver": "^7.0.1", "jwt-decode": "^3.1.2", "unzip-stream": "^0.3.1" } }, "sha512-HCc2jMJRAfviGFAh0FsOR/jNfWhirxl7W6z8zDtttt0GltwxBLdEIjLiweOPFl9WbyJRW1VWnPUSAixJqcWUMQ=="],
"@opencode-ai/desktop/marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="],
@@ -5993,6 +6036,8 @@
"@opencode-ai/llm/@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=="],
"@opencode-ai/script/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"@opencode-ai/session-ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="],
"@opencode-ai/ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="],
@@ -6119,6 +6164,8 @@
"ai-gateway-provider/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="],
"ai-gateway-provider/@ai-sdk/xai": ["@ai-sdk/xai@3.0.82", "", { "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-A0VFMufnVf4wODcT3SPQUUzvYXiIO1VhFuXj9r6z/vP4rlo+QRDPw3WSTchcz93ROQWSfBE3I6Szqz342OHi5w=="],
"ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.8.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Y6j3yivgoEUf/kutD/k5GX/mzZfioRFoSx0gbQ+mIOzMaH/vJv1rCkztiuvlLw5xRYQil7oxHUZvmSfXqOx1NQ=="],
"ajv-keywords/ajv": ["ajv@6.15.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-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
@@ -6147,6 +6194,8 @@
"astro/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="],
"astro/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"astro/common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="],
"astro/diff": ["diff@5.2.2", "", {}, "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A=="],
@@ -6237,6 +6286,8 @@
"engine.io-client/ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="],
"esast-util-from-js/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"esbuild-plugin-copy/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"esbuild-plugin-copy/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
@@ -6301,6 +6352,8 @@
"md-to-react-email/marked": ["marked@7.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ=="],
"micromark-extension-mdxjs/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"miniflare/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
@@ -6333,6 +6386,8 @@
"nypm/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="],
"opencode/@ai-sdk/cerebras": ["@ai-sdk/cerebras@2.0.60", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.54", "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Rnok3cThg6awBwaDSyiZpgRpbV7pqxGYrA89LODCo5cuEHeP2h0AM0lLHP7zIkclAdXfOm4wldKi/S2T/DGCOw=="],
"opencode/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="],
"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=="],
@@ -6341,6 +6396,8 @@
"opencode/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
"opencode/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"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=="],
"openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="],
@@ -6407,6 +6464,8 @@
"slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="],
"solid-transition-size/@corvu/utils": ["@corvu/utils@0.3.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.7" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-ZWlyWEE8qV9+CB9OAyo2bTrZGXQN9ZeM+JfYv89zoR+lRACKTDuoOZEdiyL8Uc7U5dUSH1uTqKhTTnaHWb+wZA=="],
"sort-keys/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="],
"sst/aws4fetch": ["aws4fetch@1.0.18", "", {}, "sha512-3Cf+YaUl07p24MoQ46rFwulAmiyCwH2+1zw1ZyPAX5OtJ34Hh185DwB8y/qRLb6cYYYtSFJ9pthyLc0MD4e8sQ=="],
@@ -6425,6 +6484,8 @@
"tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"terser/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"thread-stream/real-require": ["real-require@1.0.0", "", {}, "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g=="],
@@ -6443,20 +6504,24 @@
"unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
"unplugin/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"unplugin/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"unused-filename/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="],
"unzipper/fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="],
"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/openai-compatible": ["@ai-sdk/openai-compatible@2.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.32" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SoPSkrL5cbNQnAljRsJ7pOzJ2FmWgnhC0lfFOda873ycCdFJL1A+h3Ib7mX2spcv3XnNaO13y/45/0RyqNWlIQ=="],
"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=="],
"venice-ai-sdk-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.32", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Kwj499fTcN9bP/AfGoPU7JWIXeP6VZqKI6omsH062c9E2G4gdjeJczkz4z/tYSkzYjLE2AI3DtZbMfs6D7vn2Q=="],
"verror/core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="],
"vite-plugin-dynamic-import/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"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=="],
"vitest/@vitest/expect": ["@vitest/expect@4.1.7", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.7", "@vitest/utils": "4.1.7", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w=="],
@@ -6537,6 +6602,8 @@
"@ai-sdk/vercel/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/xai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@astrojs/check/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"@astrojs/check/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
@@ -6875,6 +6942,8 @@
"@standard-community/standard-openapi/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@storybook/csf-plugin/unplugin/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"@storybook/csf-plugin/unplugin/webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
@@ -6897,6 +6966,8 @@
"ai-gateway-provider/@ai-sdk/anthropic/@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-gateway-provider/@ai-sdk/xai/@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=="],
"ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
@@ -7041,6 +7112,12 @@
"opencode-gitlab-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
"opencode/@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.54", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OyXt0zK8y2/ZIyWlbxTv2r1M7AK227S+Gl4BYOEF42q0wz1n5m4fwR8L4Fy/MQ4Ho6xje47MPsFcRdIqIyP6Rw=="],
"opencode/@ai-sdk/cerebras/@ai-sdk/provider": ["@ai-sdk/provider@3.0.12", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-sj9DWTJ2Ze0WR9qsiOPqoqzNx3OxL6iMxHImbhvoe9qOspekbzxNDMiJ4TIGfYHYh9w4OmBjz3prvqhzTi96+Q=="],
"opencode/@ai-sdk/cerebras/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.33", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-nJ0bAfegMAIJtrzMJtbzer1cS3nb7c7DsyU1S4nrPm7ZU0Mn6SBBZv5IGZZGTbpWTJwqKTSPeZJTXalbAxt1BA=="],
"p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
@@ -7073,6 +7150,10 @@
"unzipper/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
"venice-ai-sdk-provider/@ai-sdk/openai-compatible/@ai-sdk/provider": ["@ai-sdk/provider@3.0.12", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-sj9DWTJ2Ze0WR9qsiOPqoqzNx3OxL6iMxHImbhvoe9qOspekbzxNDMiJ4TIGfYHYh9w4OmBjz3prvqhzTi96+Q=="],
"venice-ai-sdk-provider/@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@3.0.12", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-sj9DWTJ2Ze0WR9qsiOPqoqzNx3OxL6iMxHImbhvoe9qOspekbzxNDMiJ4TIGfYHYh9w4OmBjz3prvqhzTi96+Q=="],
"venice-ai-sdk-provider/@ai-sdk/provider-utils/@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=="],
@@ -7369,6 +7450,8 @@
"js-beautify/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"opencode/@ai-sdk/cerebras/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
"pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
+4
View File
@@ -67,6 +67,10 @@ const athenaWorkgroup = new aws.athena.Workgroup("LakeAthenaWorkgroup", {
configuration: {
enforceWorkgroupConfiguration: true,
publishCloudwatchMetricsEnabled: true,
// Athena bills $5/TB scanned; kill any query that would scan more than 2 TB
// so a regression cannot silently burn money. Stats sync full passes scan
// ~250 GB as of 2026-07.
bytesScannedCutoffPerQuery: 2 * 1024 ** 4,
resultConfiguration: {
outputLocation: $interpolate`s3://${athenaResultsBucket.bucket}/`,
},
+3 -1
View File
@@ -185,7 +185,9 @@ export const statSync = new sst.aws.Service("StatsSyncService", {
cluster: lakeCluster,
architecture: "arm64",
cpu: "0.25 vCPU",
memory: "0.5 GB",
// 0.5 GB caused an OOM crash loop: every restart immediately re-ran the 4 Athena
// stats queries (~$5/pass) every ~5 minutes instead of hourly.
memory: "2 GB",
image: {
context: ".",
dockerfile: "packages/stats/server/Dockerfile",
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-JXQ9PAqqRlJtHa8T3ZxqdRRyxC+0ip+2wSnehvKXUbI=",
"aarch64-linux": "sha256-nI+RaxDXmAcjhSjCtIvyi292xBEg0E5NXaoyGrJE69s=",
"aarch64-darwin": "sha256-UleKpm7Khf3kibm4BqZJ9bmu0N2kOjNd77g1Bd2tmfw=",
"x86_64-darwin": "sha256-V4AQH868dOfVhEj1mKTnZlhpZwFqBYsHS28Tx9VXHkE="
"x86_64-linux": "sha256-JTtn+wXTXg+yklvIMDLcGFaYhTU6ZrCgKT9JTNEQ3gA=",
"aarch64-linux": "sha256-gXU6zyhvAZrZirkL/PlHdkHtEof/7PVSPCaE34Jnd4U=",
"aarch64-darwin": "sha256-Q0oTG3uzOlD/X2kJingLle529lKFoTpyCW2rHXOZ6iE=",
"x86_64-darwin": "sha256-LINvKHxPibTlJeNzfACQx0x+Yj5oROT6Du3I5AtqqXk="
}
}
+7 -5
View File
@@ -19,6 +19,7 @@
"prepare": "husky",
"random": "echo 'Random script'",
"sso": "aws sso login --sso-session=opencode --no-browser",
"translate:app": "bun run script/translate-app.ts",
"test": "echo 'do not run tests from root' && exit 1"
},
"workspaces": {
@@ -39,13 +40,14 @@
"@octokit/rest": "22.0.0",
"@hono/standard-validator": "0.2.0",
"@hono/zod-validator": "0.4.2",
"@opentui/core": "0.3.4",
"@opentui/keymap": "0.3.4",
"@opentui/solid": "0.3.4",
"@opentui/core": "0.4.3",
"@opentui/keymap": "0.4.3",
"@opentui/solid": "0.4.3",
"@tanstack/solid-virtual": "3.13.28",
"@shikijs/stream": "4.2.0",
"ulid": "3.0.1",
"@kobalte/core": "0.13.11",
"@corvu/drawer": "0.2.4",
"@types/luxon": "3.7.1",
"@types/node": "24.12.2",
"@types/semver": "7.7.1",
@@ -104,7 +106,7 @@
"prettier": "3.6.2",
"semver": "^7.6.0",
"sst": "catalog:",
"turbo": "2.8.13"
"turbo": "2.10.2"
},
"dependencies": {
"@aws-sdk/client-s3": "3.933.0",
@@ -146,7 +148,7 @@
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
"@ai-sdk/xai@3.0.82": "patches/@ai-sdk%2Fxai@3.0.82.patch",
"@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch",
"gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch",
"pacote@21.5.0": "patches/pacote@21.5.0.patch",
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
@@ -0,0 +1,61 @@
# Timeline Layout Continuity
Run from `packages/app`:
```sh
bun run test:stability
```
The suite runs a production build in one Chromium worker. Selected scenarios use deterministic 4x CPU stress after application readiness. This is a stress profile, not emulation of a specific device.
## What It Proves
The continuity probe samples DOM-derived layout and visibility state across browser render opportunities. Tests declare explicit contracts such as:
- Preserve a visible semantic anchor while the user is away from the bottom.
- Preserve end anchoring while active content grows or new content appears.
- Keep adjacent visible rows ordered without material overlap.
- Keep user-selected disclosure state through updates and virtualization.
- Avoid a sampled blank interval while one visible surface replaces another.
- Preserve logical row and control identity where local state or focus depends on it.
- Keep keyboard, wheel, and nested-scroll ownership consistent during remeasurement.
The suite exercises real browser reducer, projection, component, virtualizer, layout, focus, and interaction code. The backend and event producer are controlled fixtures.
## What It Does Not Prove
The pass/fail oracle does not inspect every compositor-presented pixel. A sample taken after `requestAnimationFrame` is a DOM/layout observation, not proof that every sampled state was displayed or that every displayed frame was sampled.
The suite does not provide complete coverage for:
- Compositor-only or raster-only glitches.
- Color, contrast, canvas, WebGL, masks, irregular clips, or arbitrary occlusion.
- Physical display refresh rates, native OS scaling, or a named low-end device.
- TCP packetization, proxy buffering, or the complete real server/provider pipeline.
Playwright video, trace, screenshots, and observation JSON are diagnostic evidence. They are not pixel baselines and do not participate in normal pass/fail decisions.
For optional before/violation/after screenshots, set `OPENCODE_STABILITY_CAPTURE=1`. Capture is opt-in because compositor readback can perturb timing.
## Test Layers
- **Projection:** admitted rows, grouping, labels, and final visible states.
- **Local state:** disclosure state, identity, duplicate delivery, and virtualization restoration.
- **Interaction:** wheel, keyboard, nested scrolling, actionability, and focus behavior.
- **Layout continuity:** anchoring, adjacency, responsive reflow, and visible surface handoffs.
- **Reducer hardening:** validly shaped but intentionally reordered, duplicated, removed, or replaced events.
- **Oracle contract:** pure analyzer and browser sampler calibration tests.
Production-lifecycle fixtures should model states emitted by the current producer. Impossible or reordered sequences belong in reducer-hardening tests and must not be described as normal provider behavior.
## Diagnostics
Failures retain:
- `video.webm`
- `trace.zip`
- failure screenshot
- sampled DOM/layout trace JSON
- event markers and summarized violations
The analyzer records both unclipped layout bounds and ancestor-clipped visible intersections. Scrollbar and raw `scrollTop` changes alone do not fail continuity checks; user-visible semantic anchor movement does.
@@ -0,0 +1,250 @@
import { expect, test } from "@playwright/test"
import {
defineVisualRegions,
reportVisualStability,
startVisualProbe,
stopVisualProbe,
visualPlan,
} from "../../utils/visual-stability"
import {
assistantMessage,
partUpdated,
setupTimeline,
shell,
textPart,
toolPart,
userMessage,
waitForVisualSettle,
type TimelineMessage,
} from "./fixture"
test.describe("timeline adverse visual stability", () => {
test("does not pull a scrolled-away user while an active shell grows", async ({ page }, testInfo) => {
const activeShellID = "prt_adverse_01_shell"
const messages = [
...history(24),
userMessage(),
assistantMessage([shell(activeShellID, "running")], { completed: false }),
]
const timeline = await setupTimeline(page, {
messages,
settings: { shellToolPartsExpanded: true },
cpuRate: 4,
eventRetry: 30,
})
const scroller = page.locator(".scroll-view__viewport", {
has: page.locator('[data-timeline-row="AssistantPart"]'),
})
await scroller.evaluate((element) => {
element.dispatchEvent(new WheelEvent("wheel", { bubbles: true, cancelable: true, deltaY: -450 }))
element.scrollTop = Math.max(0, element.scrollHeight - element.clientHeight - 450)
})
await page.waitForTimeout(150)
await expect
.poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop))
.toBeGreaterThan(100)
const anchor = await scroller.evaluate((element) => {
const view = element.getBoundingClientRect()
return [...element.querySelectorAll<HTMLElement>("[data-timeline-key]")].find((row) => {
const rect = row.getBoundingClientRect()
return rect.top >= view.top + 40 && rect.bottom <= view.bottom - 40
})?.dataset.timelineKey
})
expect(anchor).toBeTruthy()
await waitForVisualSettle(page, [`[data-timeline-key="${anchor}"]`])
const regions = defineVisualRegions({
anchor: { selector: `[data-timeline-key="${anchor}"]` },
})
await startVisualProbe(page, regions)
await timeline.send(partUpdated(shell(activeShellID, "running", lines(1))), 180)
await timeline.send(partUpdated(shell(activeShellID, "running", lines(10))), 90)
await timeline.send(partUpdated(shell(activeShellID, "running", lines(50))), 350)
await timeline.send(partUpdated(shell(activeShellID, "completed", lines(50))), 500)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"scrolled-away-shell",
trace,
visualPlan(regions, [
{ type: "required", regions: ["anchor"] },
{ type: "unique", regions: ["anchor"] },
{ type: "stable", regions: ["anchor"] },
{ type: "fixed", regions: ["anchor"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 0 },
{ type: "label-stability", regions: "all" },
]),
)
})
test("preserves an explicit shell state across virtualization", async ({ page }) => {
const targetID = "prt_virtual_shell"
const messages = [
userMessage(undefined, { id: "msg_0000_virtual_user", created: 1700000000000 }),
assistantMessage([shell(targetID, "completed", lines(20))], {
id: "msg_0001_virtual_assistant",
parentID: "msg_0000_virtual_user",
created: 1700000001000,
}),
...history(35, 10),
]
await setupTimeline(page, { messages, settings: { shellToolPartsExpanded: false } })
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
await scroller.evaluate((element) => {
element.dispatchEvent(new WheelEvent("wheel", { bubbles: true, cancelable: true, deltaY: -1_000 }))
element.scrollTop = 0
})
await page.waitForTimeout(300)
const trigger = page.locator(`[data-timeline-part-id="${targetID}"] [data-slot="collapsible-trigger"]`)
await expect(trigger).toBeVisible()
await trigger.click()
await expect(trigger).toHaveAttribute("aria-expanded", "true")
await scroller.evaluate((element) => (element.scrollTop = element.scrollHeight))
await expect(page.locator(`[data-timeline-part-id="${targetID}"]`)).toHaveCount(0)
await scroller.evaluate((element) => (element.scrollTop = 0))
await expect(trigger).toBeVisible()
await expect(trigger).toHaveAttribute("aria-expanded", "true")
})
test("keeps narrow viewport rows ordered during long shell growth", async ({ page }, testInfo) => {
const shellID = "prt_narrow_01_shell"
const followingID = "prt_narrow_02_following"
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage(
[shell(shellID, "running"), textPart(followingID, "A narrow following row that wraps across lines.")],
{
completed: false,
},
),
],
settings: { shellToolPartsExpanded: true },
viewport: { width: 430, height: 800 },
cpuRate: 4,
})
await waitForVisualSettle(page, [
`[data-timeline-part-id="${shellID}"]`,
`[data-timeline-part-id="${followingID}"]`,
])
const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
following: {
selector: `[data-timeline-part-id="${followingID}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
})
await startVisualProbe(page, regions)
await timeline.send(partUpdated(shell(shellID, "running", wideLines(10))), 100)
await timeline.send(partUpdated(shell(shellID, "running", wideLines(50))), 300)
await timeline.send(partUpdated(shell(shellID, "completed", wideLines(50))), 500)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"narrow-shell",
trace,
visualPlan(
regions,
[
{ type: "required", regions: ["shell", "following"] },
{ type: "unique", regions: ["shell", "following"] },
{ type: "stable", regions: ["shell", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 0 },
{ type: "label-stability", regions: "all" },
{ type: "preserve-bottom-anchor" },
{ type: "flow", regions: ["shell", "following"] },
],
{ perMarker: true },
),
)
})
test("keeps visible rows ordered while resizing desktop to narrow and back", async ({ page }, testInfo) => {
const shellID = "prt_resize_01_shell"
const contextIDs = ["prt_resize_02_read", "prt_resize_03_glob"]
const followingID = "prt_resize_04_following"
await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([
shell(shellID, "completed", wideLines(15)),
toolPart(contextIDs[0]!, "read", "completed", { filePath: "src/a.ts" }),
toolPart(contextIDs[1]!, "glob", "completed", { path: ".", pattern: "**/*.ts" }),
textPart(followingID, "Following responsive timeline content that wraps on narrow screens."),
]),
],
settings: { shellToolPartsExpanded: true },
cpuRate: 4,
seedHistory: true,
})
const group = `[data-timeline-part-ids="${contextIDs.join(",")}"]`
const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
context: { selector: group, closest: '[data-timeline-row="AssistantPart"]' },
following: {
selector: `[data-timeline-part-id="${followingID}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
})
await startVisualProbe(page, regions)
await page.setViewportSize({ width: 430, height: 800 })
await page.waitForTimeout(500)
await page.setViewportSize({ width: 900, height: 800 })
await page.waitForTimeout(500)
await page.setViewportSize({ width: 1400, height: 900 })
await page.waitForTimeout(500)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"responsive-resize",
trace,
visualPlan(regions, [
{ type: "required", regions: ["shell", "context", "following"] },
{ type: "unique", regions: ["shell", "context", "following"] },
{ type: "stable", regions: ["shell", "context", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 4, maxReversals: 4 },
{ type: "label-stability", regions: "all" },
{ type: "flow", regions: ["shell", "context", "following"] },
]),
)
})
})
function history(count: number, offset = 0): TimelineMessage[] {
return Array.from({ length: count }, (_, index) => {
const value = index + offset
const prefix = `msg_0${String(value).padStart(3, "0")}_history`
const userID = `${prefix}_a_user`
return [
userMessage(undefined, { id: userID, created: 1699990000000 + value * 10_000 }),
assistantMessage(
[
textPart(
`prt_history_${String(value).padStart(3, "0")}`,
`Historical response ${value}. ${"Stable history content. ".repeat(8)}`,
),
],
{
id: `${prefix}_b_assistant`,
parentID: userID,
created: 1699990001000 + value * 10_000,
},
),
]
}).flat()
}
function lines(count: number) {
return Array.from({ length: count }, (_, index) => `line ${index + 1}`).join("\n")
}
function wideLines(count: number) {
return Array.from({ length: count }, (_, index) => `line ${index + 1} ${"wide-output-".repeat(20)}`).join("\n")
}
@@ -0,0 +1,192 @@
import { expect, test } from "@playwright/test"
import {
defineVisualRegions,
reportVisualStability,
startVisualProbe,
stopVisualProbe,
visualPlan,
} from "../../utils/visual-stability"
import {
assistantID,
assistantMessage,
event,
partUpdated,
setupTimeline,
textPart,
toolPart,
userMessage,
waitForVisualSettle,
} from "./fixture"
const inputs = {
read: { filePath: "src/a.ts", offset: 0, limit: 120 },
glob: { path: ".", pattern: "**/*.ts" },
grep: { path: ".", pattern: "stable", include: "*.ts" },
list: { path: "src" },
}
test("appends context operations while the group is expanded", async ({ page }, testInfo) => {
const firstID = "prt_append_01_read"
const followingID = "prt_append_99_following"
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([toolPart(firstID, "read", "running", inputs.read), textPart(followingID, "Following append")], {
completed: false,
}),
],
cpuRate: 4,
})
const initialGroup = `[data-timeline-part-ids="${firstID}"]`
await page.locator(`${initialGroup} [data-slot="collapsible-trigger"]`).click()
await waitForVisualSettle(page, [initialGroup, `[data-timeline-part-id="${followingID}"]`])
const regions = defineVisualRegions({
context: {
selector: '[data-timeline-part-ids^="prt_append_01_read"]',
closest: '[data-timeline-row="AssistantPart"]',
},
following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
})
await startVisualProbe(page, regions)
await timeline.send(partUpdated(toolPart("prt_append_02_glob", "glob", "running", inputs.glob)), 180)
await timeline.send(partUpdated(toolPart("prt_append_03_grep", "grep", "completed", inputs.grep)), 240)
await timeline.send(partUpdated(toolPart("prt_append_04_list", "list", "completed", inputs.list)), 500)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"context-append",
trace,
visualPlan(
regions,
[
{ type: "required", regions: ["context", "following"] },
{ type: "unique", regions: ["context", "following"] },
{ type: "stable", regions: ["context", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: ["following"], maxPositionReversals: 0 },
{ type: "label-stability", regions: "all" },
{ type: "preserve-bottom-anchor" },
{ type: "flow", regions: ["context", "following"] },
],
{ perMarker: true },
),
)
await expect(
page.locator(
'[data-timeline-part-ids="prt_append_01_read,prt_append_02_glob,prt_append_03_grep,prt_append_04_list"]',
),
).toBeVisible()
await expect(
page.locator('[data-timeline-part-ids^="prt_append_01_read"] [data-slot="collapsible-trigger"]'),
).toHaveAttribute("aria-expanded", "true")
})
test("splits and merges context groups when a middle text part changes", async ({ page }, testInfo) => {
const textID = "prt_split_02_text"
const followingID = "prt_split_99_following"
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([
toolPart("prt_split_01_read", "read", "completed", inputs.read),
textPart(textID, "Boundary"),
toolPart("prt_split_03_glob", "glob", "completed", inputs.glob),
textPart(followingID, "Following split groups"),
]),
],
cpuRate: 4,
})
const regions = defineVisualRegions({
following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
})
await startVisualProbe(page, regions)
await timeline.send(
event("message.part.removed", { sessionID: "ses_timeline_stability", messageID: assistantID, partID: textID }),
500,
)
await expect(page.locator('[data-timeline-part-ids="prt_split_01_read,prt_split_03_glob"]')).toBeVisible()
await timeline.send(partUpdated(textPart(textID, "Boundary restored")), 500)
await expect(page.locator('[data-timeline-part-ids="prt_split_01_read"]')).toBeVisible()
await expect(page.locator('[data-timeline-part-ids="prt_split_03_glob"]')).toBeVisible()
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"context-split-merge",
trace,
visualPlan(
regions,
[
{ type: "required", regions: ["following"] },
{ type: "unique", regions: ["following"] },
{ type: "stable", regions: ["following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 1 },
{ type: "label-stability", regions: "all" },
],
{ perMarker: true },
),
)
})
test("removing the first context member replaces the group once without overlapping following content", async ({
page,
}, testInfo) => {
const ids = ["prt_key_01_read", "prt_key_02_glob", "prt_key_03_grep"]
const followingID = "prt_key_99_following"
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([
toolPart(ids[0]!, "read", "completed", inputs.read),
toolPart(ids[1]!, "glob", "completed", inputs.glob),
toolPart(ids[2]!, "grep", "completed", inputs.grep),
textPart(followingID, "Following replaced group"),
]),
],
cpuRate: 4,
})
const original = page.locator(`[data-timeline-part-ids="${ids.join(",")}"]`)
const originalRowKey = await original.evaluate((element) =>
element.closest("[data-timeline-key]")?.getAttribute("data-timeline-key"),
)
await original.locator('[data-slot="collapsible-trigger"]').click()
await expect(original.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "true")
const regions = defineVisualRegions({
context: {
selector: '[data-timeline-part-ids*="prt_key_02_glob"]',
closest: '[data-timeline-row="AssistantPart"]',
},
following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
})
await startVisualProbe(page, regions)
await timeline.send(
event("message.part.removed", { sessionID: "ses_timeline_stability", messageID: assistantID, partID: ids[0] }),
500,
)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"context-first-remove",
trace,
visualPlan(regions, [
{ type: "required", regions: ["context", "following"] },
{ type: "unique", regions: ["context", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 0 },
{ type: "label-stability", regions: "all" },
{ type: "flow", regions: ["context", "following"] },
]),
)
await expect(page.locator(`[data-timeline-part-ids="${ids.slice(1).join(",")}"]`)).toBeVisible()
expect(
await page
.locator(`[data-timeline-part-ids="${ids.slice(1).join(",")}"]`)
.evaluate((element) => element.closest("[data-timeline-key]")?.getAttribute("data-timeline-key")),
).toBe(originalRowKey)
await expect(
page.locator(`[data-timeline-part-ids="${ids.slice(1).join(",")}"] [data-slot="collapsible-trigger"]`),
).toHaveAttribute("aria-expanded", "true")
})
@@ -0,0 +1,113 @@
import { test } from "@playwright/test"
import {
defineVisualRegions,
reportVisualStability,
startVisualProbe,
stopVisualProbe,
visualPlan,
} from "../../utils/visual-stability"
import {
assistantMessage,
partUpdated,
setupTimeline,
shell,
textPart,
userMessage,
waitForVisualSettle,
} from "./fixture"
// Fractional scaling exercises different browser rounding than the baseline.
for (const deviceScaleFactor of [1, 1.25]) {
test(`keeps shell growth ordered at device scale ${deviceScaleFactor}`, async ({ page }, testInfo) => {
const shellID = `prt_dpr_${String(deviceScaleFactor).replace(".", "_")}_01_shell`
const followingID = `prt_dpr_${String(deviceScaleFactor).replace(".", "_")}_02_following`
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([shell(shellID, "running"), textPart(followingID, "Following scaled shell")], {
completed: false,
}),
],
settings: { shellToolPartsExpanded: true },
cpuRate: 4,
deviceScaleFactor,
seedHistory: true,
})
await waitForVisualSettle(page, [
`[data-timeline-part-id="${shellID}"]`,
`[data-timeline-part-id="${followingID}"]`,
])
const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
following: {
selector: `[data-timeline-part-id="${followingID}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
})
await startVisualProbe(page, regions)
await timeline.send(partUpdated(shell(shellID, "running", lines(20))), 180)
await timeline.send(partUpdated(shell(shellID, "completed", lines(20))), 500)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(testInfo, `dpr-${deviceScaleFactor}`, trace, shellPlan(regions))
})
}
for (const reducedMotion of [true]) {
test(`keeps shell and status transitions ordered with reduced motion ${reducedMotion}`, async ({
page,
}, testInfo) => {
const shellID = `prt_motion_${reducedMotion}_01_shell`
const followingID = `prt_motion_${reducedMotion}_02_following`
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([shell(shellID, "running"), textPart(followingID, "Following motion profile")], {
completed: false,
}),
],
settings: { shellToolPartsExpanded: true },
reducedMotion,
cpuRate: 4,
seedHistory: true,
})
await waitForVisualSettle(page, [
`[data-timeline-part-id="${shellID}"]`,
`[data-timeline-part-id="${followingID}"]`,
])
const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
following: {
selector: `[data-timeline-part-id="${followingID}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
})
await startVisualProbe(page, regions)
await timeline.send(partUpdated(shell(shellID, "completed", lines(10))), 500)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(testInfo, `reduced-motion-${reducedMotion}`, trace, shellPlan(regions))
})
}
function lines(count: number) {
return Array.from({ length: count }, (_, index) => `line ${index + 1}`).join("\n")
}
function shellPlan<Regions extends ReturnType<typeof defineVisualRegions>>(
regions: Regions & Record<"shell" | "following", { selector: string }>,
) {
return visualPlan(
regions,
[
{ type: "required", regions: ["shell", "following"] },
{ type: "unique", regions: ["shell", "following"] },
{ type: "stable", regions: ["shell", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 0 },
{ type: "label-stability", regions: "all" },
{ type: "preserve-bottom-anchor" },
{ type: "flow", regions: ["shell", "following"] },
],
{ perMarker: true },
)
}
@@ -0,0 +1,121 @@
import { test } from "@playwright/test"
import {
defineVisualRegions,
reportVisualStability,
startVisualProbe,
stopVisualProbe,
visualPlan,
} from "../../utils/visual-stability"
import {
assistantMessage,
partUpdated,
setupTimeline,
textPart,
toolPart,
userMessage,
waitForVisualSettle,
} from "./fixture"
const profiles = [
{ name: "edit", tool: "edit", input: { filePath: "src/edit.ts" } },
{
name: "multi patch",
tool: "apply_patch",
input: { files: ["src/a.ts", "src/b.ts", "src/old.ts", "src/moved.ts"] },
},
] as const
for (const profile of profiles) {
test(`stabilizes ${profile.name} pending to completed`, async ({ page }, testInfo) => {
const partID = `prt_file_matrix_${profiles.indexOf(profile)}`
const followingID = `prt_file_matrix_following_${profiles.indexOf(profile)}`
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage(
[
toolPart(partID, profile.tool, "pending", profile.input),
textPart(followingID, `Following ${profile.name}`),
],
{ completed: false },
),
],
settings: { editToolPartsExpanded: true },
cpuRate: 4,
})
await waitForVisualSettle(page, [`[data-timeline-part-id="${partID}"]`, `[data-timeline-part-id="${followingID}"]`])
const regions = defineVisualRegions({
tool: { selector: `[data-timeline-part-id="${partID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
following: {
selector: `[data-timeline-part-id="${followingID}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
})
await startVisualProbe(page, regions)
await timeline.send(partUpdated(toolPart(partID, profile.tool, "running", profile.input)), 180)
await timeline.send(partUpdated(completedPart(partID, profile)), 900)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
`file-${profile.name}`,
trace,
visualPlan(
regions,
[
{ type: "required", regions: ["tool", "following"] },
{ type: "unique", regions: ["tool", "following"] },
{ type: "stable", regions: ["tool", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 0, maxReversals: 1 },
{ type: "label-stability", regions: "all" },
{ type: "preserve-bottom-anchor" },
{ type: "flow", regions: ["tool", "following"] },
],
{ perMarker: true },
),
)
})
}
function completedPart(partID: string, profile: (typeof profiles)[number]) {
if (profile.tool === "edit") {
return toolPart(partID, profile.tool, "completed", profile.input, {
metadata: {
filediff: {
file: "src/edit.ts",
additions: 50,
deletions: 50,
before: source(50, false),
after: source(50, true),
},
},
})
}
const files = [
patchFile("src/a.ts", "update"),
patchFile("src/b.ts", "add"),
patchFile("src/old.ts", "delete"),
{ ...patchFile("src/moved.ts", "move"), move: "src/new-place.ts" },
]
return toolPart(partID, profile.tool, "completed", profile.input, { metadata: { files } })
}
function patchFile(filePath: string, type: "add" | "update" | "delete" | "move") {
return {
filePath,
relativePath: filePath,
type,
additions: type === "delete" ? 0 : 20,
deletions: type === "add" ? 0 : 20,
before: type === "add" ? undefined : source(20, false),
after: type === "delete" ? undefined : source(20, true),
}
}
function source(count: number, changed: boolean) {
return Array.from(
{ length: count },
(_, index) => `export const value${index} = ${changed ? index + 1 : index}\n`,
).join("")
}
@@ -0,0 +1,113 @@
import { expect, test } from "@playwright/test"
import {
defineVisualRegions,
reportVisualStability,
startVisualProbe,
stopVisualProbe,
visualPlan,
} from "../../utils/visual-stability"
import {
assistantMessage,
partUpdated,
setupTimeline,
textPart,
toolPart,
userMessage,
waitForVisualSettle,
} from "./fixture"
test("adds patch files incrementally without resetting outer expansion", async ({ page }, testInfo) => {
const patchID = "prt_incremental_01_patch"
const followingID = "prt_incremental_02_following"
const first = patchFile("src/a.ts", "update")
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage(
[
toolPart(patchID, "apply_patch", "running", { files: [first.filePath] }, { metadata: { files: [first] } }),
textPart(followingID, "Following incremental patch"),
],
{ completed: false },
),
],
settings: { editToolPartsExpanded: true },
cpuRate: 4,
seedHistory: true,
})
const trigger = page.locator(`[data-timeline-part-id="${patchID}"] [data-slot="collapsible-trigger"]`).first()
await expect(trigger).toHaveAttribute("aria-expanded", "true")
await waitForVisualSettle(page, [`[data-timeline-part-id="${patchID}"]`, `[data-timeline-part-id="${followingID}"]`])
const regions = defineVisualRegions({
patch: { selector: `[data-timeline-part-id="${patchID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
})
await startVisualProbe(page, regions)
const second = patchFile("src/b.ts", "add")
const third = patchFile("src/old.ts", "delete")
await timeline.send(
partUpdated(
toolPart(
patchID,
"apply_patch",
"running",
{ files: [first.filePath, second.filePath] },
{ metadata: { files: [first, second] } },
),
),
240,
)
await timeline.send(
partUpdated(
toolPart(
patchID,
"apply_patch",
"completed",
{ files: [first.filePath, second.filePath, third.filePath] },
{ metadata: { files: [first, second, third] } },
),
),
800,
)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"incremental-patch",
trace,
visualPlan(
regions,
[
{ type: "required", regions: ["patch", "following"] },
{ type: "unique", regions: ["patch", "following"] },
{ type: "stable", regions: ["patch", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: ["following"], maxPositionReversals: 0 },
{ type: "label-stability", regions: "all" },
{ type: "preserve-bottom-anchor" },
{ type: "flow", regions: ["patch", "following"] },
],
{ perMarker: true },
),
)
await expect(trigger).toHaveAttribute("aria-expanded", "true")
await expect(page.locator('[data-scope="apply-patch"] [data-type="delete"]')).toBeVisible()
})
function patchFile(filePath: string, type: "add" | "update" | "delete") {
return {
filePath,
relativePath: filePath,
type,
additions: type === "delete" ? 0 : 4,
deletions: type === "add" ? 0 : 3,
before: type === "add" ? undefined : source(false),
after: type === "delete" ? undefined : source(true),
}
}
function source(changed: boolean) {
return Array.from({ length: 12 }, (_, index) => `export const value${index} = ${changed ? index + 1 : index}\n`).join(
"",
)
}
@@ -0,0 +1,68 @@
import { describe, expect, test } from "bun:test"
import {
assistantMessage,
event,
toolPart,
userMessage,
validateTimelineEvent,
validateTimelineMessages,
type PartSeed,
} from "./fixture"
describe("timeline fixture validation", () => {
test("accepts a valid timeline", () => {
expect(validateTimelineMessages([userMessage(), assistantMessage()])).toHaveLength(2)
})
test("rejects malformed SDK values at runtime", () => {
expect(() =>
assistantMessage([], {
error: { name: "APIError", data: { message: "failed" } } as never,
}),
).toThrow()
expect(() =>
validateTimelineEvent({
directory: "C:/OpenCode/TimelineStability",
payload: {
id: "evt_invalid_status",
type: "session.status",
properties: { sessionID: "ses_timeline_stability", status: { type: "retry", attempt: 1 } },
},
}),
).toThrow()
})
test("rejects duplicate IDs and orphan assistants", () => {
expect(() => validateTimelineMessages([userMessage(), userMessage()])).toThrow(/duplicate message ID/)
expect(() =>
validateTimelineMessages([userMessage(), assistantMessage([], { parentID: "msg_missing_parent" })]),
).toThrow(/parent user/)
})
test("assigns deterministic event IDs", () => {
const first = event("session.status", { sessionID: "ses_timeline_stability", status: { type: "busy" } })
const second = event("session.status", { sessionID: "ses_timeline_stability", status: { type: "idle" } })
expect(first.payload.id).toMatch(/^evt_timeline_\d{4}$/)
expect(Number(second.payload.id.slice(-4))).toBe(Number(first.payload.id.slice(-4)) + 1)
})
})
if (false) {
const userSeed = { id: "prt_type_user", type: "text", text: "typed" } satisfies PartSeed<"user">
userMessage([userSeed])
// @ts-expect-error Tool completion fields are not valid while pending.
toolPart("prt_invalid_pending", "bash", "pending", {}, { output: "impossible" })
// @ts-expect-error Tool completion fields are not valid while running.
toolPart("prt_invalid_running", "bash", "running", {}, { output: "impossible" })
// @ts-expect-error Tool error fields are not valid after completion.
toolPart("prt_invalid_completed", "bash", "completed", {}, { error: "impossible" })
assistantMessage([
// @ts-expect-error Agent references belong to user messages, not assistant messages.
{ id: "prt_invalid_owner", type: "agent", name: "explore", source: { value: "@explore", start: 0, end: 8 } },
])
// @ts-expect-error Retry status events require message and next.
event("session.status", { sessionID: "ses_timeline_stability", status: { type: "retry", attempt: 1 } })
}
@@ -0,0 +1,561 @@
import { base64Encode } from "@opencode-ai/core/util/encode"
import { Event } from "@opencode-ai/schema/event"
import { SessionStatusEvent } from "@opencode-ai/schema/session-status-event"
import { SessionV1 } from "@opencode-ai/schema/session-v1"
import type {
AssistantMessage,
GlobalEvent,
Message,
Part,
Session,
SessionStatus,
ToolPart,
ToolState,
UserMessage,
} from "@opencode-ai/sdk/v2/client"
import { expect, type Page } from "@playwright/test"
import { Schema } from "effect"
import { mockOpenCodeServer } from "../../utils/mock-server"
import { installSseTransport } from "../../utils/sse-transport"
import { expectSessionTitle } from "../../utils/waits"
export const directory = "C:/OpenCode/TimelineStability"
export const projectID = "proj_timeline_stability"
export const sessionID = "ses_timeline_stability"
export const userID = "msg_1000_timeline_user"
export const assistantID = "msg_1001_timeline_assistant"
export const title = "Timeline visual stability"
export const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" }
type TimelinePayload = Extract<
GlobalEvent["payload"],
{
type:
| "message.updated"
| "message.removed"
| "message.part.updated"
| "message.part.removed"
| "message.part.delta"
| "session.status"
}
>
type DeepReadonly<Value> = Value extends readonly unknown[]
? { readonly [Key in keyof Value]: DeepReadonly<Value[Key]> }
: Value extends object
? { readonly [Key in keyof Value]: DeepReadonly<Value[Key]> }
: Value
export type TimelineEvent = DeepReadonly<Omit<GlobalEvent, "payload"> & { payload: TimelinePayload }>
export type EventPayload = TimelineEvent
export type ToolStatus = ToolState["status"]
export type TimelineMessage = { info: UserMessage; parts: Part[] } | { info: AssistantMessage; parts: Part[] }
type UserPart = Extract<Part, { type: "text" | "file" | "agent" | "subtask" }>
type AssistantPart = Exclude<Part, { type: "agent" | "subtask" }>
type OwnedPart<Owner extends Message["role"]> = Owner extends "user" ? UserPart : AssistantPart
export type PartSeed<Owner extends Message["role"]> =
OwnedPart<Owner> extends infer Candidate
? Candidate extends Part
? Omit<Candidate, "sessionID" | "messageID">
: never
: never
type ToolOptions<State extends ToolStatus> = State extends "pending"
? { output?: never; title?: never; metadata?: never; error?: never }
: State extends "running"
? { title?: string; metadata?: Record<string, unknown>; output?: never; error?: never }
: State extends "error"
? { error?: string; metadata?: Record<string, unknown>; output?: never; title?: never }
: { output?: string; title?: string; metadata?: Record<string, unknown>; error?: never }
const decodeOptions = { errors: "all", onExcessProperty: "error" } as const
const decodeMessage = Schema.decodeUnknownSync(SessionV1.WithParts)
const decodePart = Schema.decodeUnknownSync(SessionV1.Part)
const decodeStatus = Schema.decodeUnknownSync(SessionStatusEvent.Info)
const timelineEventSchema = Schema.Union([
eventSchema("message.updated", SessionV1.Event.MessageUpdated.data),
eventSchema("message.removed", SessionV1.Event.MessageRemoved.data),
eventSchema("message.part.updated", SessionV1.Event.PartUpdated.data),
eventSchema("message.part.removed", SessionV1.Event.PartRemoved.data),
eventSchema("message.part.delta", SessionV1.Event.PartDelta.data),
eventSchema("session.status", SessionStatusEvent.Status.data),
])
const decodeEvent = Schema.decodeUnknownSync(timelineEventSchema)
let eventSequence = 0
export async function setupTimeline(
page: Page,
input: {
messages?: TimelineMessage[]
settings?: Record<string, boolean>
sessions?: Session[]
cpuRate?: number
viewport?: { width: number; height: number }
eventRetry?: number
reducedMotion?: boolean
locale?: string
deviceScaleFactor?: number
seedHistory?: boolean
} = {},
) {
const sessions = input.sessions ?? [session()]
const messages = validateTimelineMessages([
...(input.seedHistory ? historyMessages(18) : []),
...(input.messages ?? [userMessage(), assistantMessage()]),
])
const active = messages.findLast((message) => message.info.role === "assistant")
const initialStatus = decodeStatus(
active?.info.role === "assistant" && active.info.time.completed === undefined ? { type: "busy" } : { type: "idle" },
decodeOptions,
)
const transport = await installSseTransport<EventPayload>(page, {
server: `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`,
retry: input.eventRetry ?? 20,
})
await mockOpenCodeServer(page, {
directory,
project: project(),
provider: provider(),
sessions,
sessionStatus: { [sessionID]: initialStatus },
pageMessages: () => ({
items: messages,
}),
})
await page.addInitScript((settings) => {
localStorage.setItem(
"settings.v3",
JSON.stringify({
general: {
editToolPartsExpanded: false,
shellToolPartsExpanded: false,
showReasoningSummaries: false,
showSessionProgressBar: true,
...settings,
},
}),
)
}, input.settings ?? {})
if (input.locale) {
await page.addInitScript((locale) => {
localStorage.setItem("opencode.global.dat:language", JSON.stringify({ locale }))
}, input.locale)
}
if (input.reducedMotion) await page.emulateMedia({ reducedMotion: "reduce" })
await page.setViewportSize(input.viewport ?? { width: 1400, height: 900 })
if (input.deviceScaleFactor) {
const devtools = await page.context().newCDPSession(page)
const viewport = input.viewport ?? { width: 1400, height: 900 }
await devtools.send("Emulation.setDeviceMetricsOverride", {
width: viewport.width,
height: viewport.height,
deviceScaleFactor: input.deviceScaleFactor,
mobile: false,
})
}
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
await transport.waitForConnection()
await expectSessionTitle(page, title)
if (input.cpuRate && input.cpuRate > 1) {
const devtools = await page.context().newCDPSession(page)
await devtools.send("Emulation.setCPUThrottlingRate", { rate: input.cpuRate })
}
return {
transport,
async send(event: TimelineEvent, delay = 0) {
const valid = validateTimelineEvent(event)
await transport.send(valid, { marker: describeEvent(valid) })
if (delay) await page.waitForTimeout(delay)
},
async sendAll(sequence: { event: TimelineEvent; delay: number }[]) {
for (const item of sequence) {
const valid = validateTimelineEvent(item.event)
await transport.send(valid, { marker: describeEvent(valid) })
await page.waitForTimeout(item.delay)
}
},
async settle(frames = 3) {
await page.evaluate(
(frames) =>
new Promise<void>((resolve) => {
let remaining = frames
const tick = () => {
remaining--
if (remaining <= 0) return resolve()
requestAnimationFrame(tick)
}
requestAnimationFrame(tick)
}),
frames,
)
},
async waitForPart(partID: string) {
await expect(page.locator(`[data-timeline-part-id="${partID}"]`).first()).toBeVisible()
},
}
}
function describeEvent(event: EventPayload) {
if (event.payload.type === "message.part.updated") {
const part = event.payload.properties.part
return [
event.payload.type,
part.id,
part.type === "tool" ? part.tool : part.type,
part.type === "tool" ? part.state.status : undefined,
]
.filter(Boolean)
.join(":")
}
if (event.payload.type === "session.status") {
const status = event.payload.properties.status
return [event.payload.type, status.type, status.type === "retry" ? status.attempt : undefined]
.filter((value) => value !== undefined)
.join(":")
}
return event.payload.type
}
export function event<const Type extends TimelinePayload["type"]>(
type: Type,
properties: Extract<TimelinePayload, { type: Type }>["properties"],
): TimelineEvent
export function event(type: TimelinePayload["type"], properties: TimelinePayload["properties"]): TimelineEvent {
return validateTimelineEvent({
directory,
payload: { id: `evt_timeline_${String(++eventSequence).padStart(4, "0")}`, type, properties },
})
}
export function validateTimelineEvent(input: unknown): TimelineEvent {
return decodeEvent(input, decodeOptions)
}
export function validateTimelineMessages(input: readonly TimelineMessage[]): TimelineMessage[] {
input.forEach((message) => decodeMessage(message, decodeOptions))
const messages = [...input]
const messageIDs = new Set<string>()
const partIDs = new Set<string>()
const users = new Set(messages.filter((message) => message.info.role === "user").map((message) => message.info.id))
messages.forEach((message) => {
if (messageIDs.has(message.info.id))
throw new Error(`Timeline fixture has duplicate message ID: ${message.info.id}`)
messageIDs.add(message.info.id)
if (message.info.role === "assistant" && !users.has(message.info.parentID))
throw new Error(`Timeline assistant ${message.info.id} must reference a parent user in the fixture`)
message.parts.forEach((part) => {
if (part.sessionID !== message.info.sessionID || part.messageID !== message.info.id)
throw new Error(`Timeline part ${part.id} ownership does not match message ${message.info.id}`)
if (message.info.role === "user" && !["text", "file", "agent", "subtask"].includes(part.type))
throw new Error(`Timeline user message ${message.info.id} cannot own ${part.type} part ${part.id}`)
if (message.info.role === "assistant" && ["agent", "subtask"].includes(part.type))
throw new Error(`Timeline assistant message ${message.info.id} cannot own ${part.type} part ${part.id}`)
if (partIDs.has(part.id)) throw new Error(`Timeline fixture has duplicate part ID: ${part.id}`)
partIDs.add(part.id)
})
})
return messages
}
export async function waitForVisualSettle(page: Page, selectors: string[], stableFrames = 3) {
await page.waitForFunction(
({ selectors, stableFrames }) => {
const elements = selectors.map((selector) => document.querySelector<HTMLElement>(selector))
if (elements.some((element) => !element)) return false
return new Promise<boolean>((resolve) => {
let stable = 0
let previous = ""
const sample = () => {
const signature = JSON.stringify(
elements.map((element) => {
const rect = element!.getBoundingClientRect()
return [Math.round(rect.top * 10), Math.round(rect.bottom * 10), Math.round(rect.height * 10)]
}),
)
stable = signature === previous ? stable + 1 : 0
previous = signature
const ordered = elements
.slice(1)
.every(
(element, index) =>
elements[index]!.getBoundingClientRect().bottom <= element!.getBoundingClientRect().top + 0.5,
)
if (stable >= stableFrames && ordered) return resolve(true)
requestAnimationFrame(sample)
}
requestAnimationFrame(sample)
})
},
{ selectors, stableFrames },
)
}
export function historyMessages(count: number): TimelineMessage[] {
return Array.from({ length: count }, (_, index) => {
const value = String(index).padStart(4, "0")
const historyUserID = `msg_0${value}_history_a_user`
return [
userMessage(undefined, { id: historyUserID, created: 1690000000000 + index * 10_000 }),
assistantMessage(
[
{
id: `prt_0${value}_history_text`,
type: "text",
text: `Historical response ${index}. ${"Existing session content keeps the virtual timeline realistic. ".repeat(5)}`,
},
],
{
id: `msg_0${value}_history_b_assistant`,
parentID: historyUserID,
created: 1690000001000 + index * 10_000,
},
),
]
}).flat()
}
export function partUpdated(part: Part | PartSeed<"assistant">) {
const owned = "messageID" in part ? part : { ...part, sessionID, messageID: assistantID }
decodePart(owned, decodeOptions)
return event("message.part.updated", {
sessionID,
part: owned,
time: 1700000002000,
})
}
export function partDelta(partID: string, delta: string, messageID = assistantID) {
return event("message.part.delta", { sessionID, messageID, partID, field: "text", delta })
}
export function messageUpdated(info: Message) {
return event("message.updated", { sessionID, info })
}
export function status(type: SessionStatus["type"], attempt = 1) {
return event("session.status", {
sessionID,
status: type === "retry" ? { type, attempt, message: "Rate limited", next: 1700000010000 } : { type },
})
}
export function userMessage(
parts?: PartSeed<"user">[],
input: { id?: string; summary?: UserMessage["summary"]; created?: number } = {},
): Extract<TimelineMessage, { info: { role: "user" } }> {
const id = input.id ?? userID
const seeds = parts ?? [userText("Build the timeline stability matrix.", { id: `prt_${id}_text` })]
const message = {
info: {
id,
sessionID,
role: "user",
time: { created: input.created ?? 1700000000000 },
summary: input.summary ?? { diffs: [] },
agent: "build",
model,
},
parts: seeds.map((part) => ({
...part,
sessionID,
messageID: id,
})),
} satisfies Extract<TimelineMessage, { info: { role: "user" } }>
decodeMessage(message, decodeOptions)
return message
}
export function assistantMessage(
parts: PartSeed<"assistant">[] = [],
input: {
id?: string
parentID?: string
completed?: boolean
error?: AssistantMessage["error"]
created?: number
} = {},
): Extract<TimelineMessage, { info: { role: "assistant" } }> {
const id = input.id ?? assistantID
const message = {
info: {
id,
sessionID,
role: "assistant",
time: {
created: input.created ?? 1700000001000,
...(input.completed === false ? {} : { completed: (input.created ?? 1700000001000) + 1_000 }),
},
parentID: input.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",
...(input.error ? { error: input.error } : {}),
},
parts: parts.map((part) => ({ ...part, sessionID, messageID: id })),
} satisfies Extract<TimelineMessage, { info: { role: "assistant" } }>
decodeMessage(message, decodeOptions)
return message
}
export function userText(
text: string,
input: Partial<Omit<Extract<PartSeed<"user">, { type: "text" }>, "type" | "text">> = {},
): Extract<PartSeed<"user">, { type: "text" }> {
return { id: "prt_user_text", type: "text", text, ...input }
}
export function textPart(id: string, text: string): Extract<PartSeed<"assistant">, { type: "text" }> {
return { id, type: "text", text }
}
export function reasoningPart(id: string, text: string): Extract<PartSeed<"assistant">, { type: "reasoning" }> {
return { id, type: "reasoning", text, time: { start: 1700000001000 } }
}
export function toolPart(
id: string,
tool: string,
state: "pending",
input: Record<string, unknown>,
options?: ToolOptions<"pending">,
): Omit<ToolPart, "sessionID" | "messageID">
export function toolPart(
id: string,
tool: string,
state: "running",
input: Record<string, unknown>,
options?: ToolOptions<"running">,
): Omit<ToolPart, "sessionID" | "messageID">
export function toolPart(
id: string,
tool: string,
state: "completed",
input: Record<string, unknown>,
options?: ToolOptions<"completed">,
): Omit<ToolPart, "sessionID" | "messageID">
export function toolPart(
id: string,
tool: string,
state: "error",
input: Record<string, unknown>,
options?: ToolOptions<"error">,
): Omit<ToolPart, "sessionID" | "messageID">
export function toolPart(
id: string,
tool: string,
state: ToolStatus,
input: Record<string, unknown>,
options: ToolOptions<ToolStatus> = {},
): Omit<ToolPart, "sessionID" | "messageID"> {
const base = { id, type: "tool" as const, callID: `call_${id}`, tool }
if (state === "pending") return { ...base, state: { status: state, input, raw: "" } }
if (state === "running")
return {
...base,
state: {
status: state,
input,
title: options.title,
metadata: options.metadata ?? {},
time: { start: 1700000001000 },
},
}
if (state === "error")
return {
...base,
state: {
status: state,
input,
error: options.error ?? "Tool failed",
metadata: options.metadata ?? {},
time: { start: 1700000001000, end: 1700000002000 },
},
}
return {
...base,
state: {
status: state,
input,
output: options.output ?? "Completed",
title: options.title ?? tool,
metadata: options.metadata ?? {},
time: { start: 1700000001000, end: 1700000002000 },
},
}
}
export function shell(
id: string,
state: ToolStatus,
output = "",
command = `echo ${id}`,
): Omit<ToolPart, "sessionID" | "messageID"> {
if (state === "pending") return toolPart(id, "bash", state, { command })
if (state === "running")
return toolPart(id, "bash", state, { command }, { title: command, metadata: { command, output } })
if (state === "error")
return toolPart(id, "bash", state, { command }, { error: output || undefined, metadata: { command, output } })
return toolPart(id, "bash", state, { command }, { title: command, output, metadata: { command, output } })
}
export function completedAssistantInfo(info: AssistantMessage): AssistantMessage {
return { ...info, time: { ...info.time, completed: 1700000003000 } }
}
export function project() {
return {
id: projectID,
worktree: directory,
vcs: "git",
name: "timeline-stability",
time: { created: 1700000000000, updated: 1700000000000 },
sandboxes: [],
}
}
export function session(input: Partial<Session> = {}): Session {
return {
id: sessionID,
slug: "timeline-stability",
projectID,
directory,
title,
version: "dev",
time: { created: 1700000000000, updated: 1700000000000 },
...input,
}
}
function eventSchema<
const Type extends TimelinePayload["type"],
const Properties extends Schema.Codec<unknown, unknown>,
>(type: Type, properties: Properties) {
return Schema.Struct({
directory: Schema.String,
project: Schema.optional(Schema.String),
workspace: Schema.optional(Schema.String),
payload: Schema.Struct({ id: Event.ID, type: Schema.Literal(type), properties }),
})
}
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" },
}
}
@@ -0,0 +1,242 @@
import { expect, test } from "@playwright/test"
import {
defineVisualRegions,
reportVisualStability,
startVisualProbe,
stopVisualProbe,
visualPlan,
} from "../../utils/visual-stability"
import { assistantMessage, setupTimeline, shell, textPart, toolPart, userMessage, waitForVisualSettle } from "./fixture"
test("expands and collapses a long completed shell without overlap", async ({ page }, testInfo) => {
const shellID = "prt_interaction_01_shell"
const followingID = "prt_interaction_02_following"
await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([shell(shellID, "completed", lines(50)), textPart(followingID, "Following shell expansion")]),
],
settings: { shellToolPartsExpanded: false },
cpuRate: 4,
seedHistory: true,
})
const trigger = page.locator(`[data-timeline-part-id="${shellID}"] [data-slot="collapsible-trigger"]`)
await waitForVisualSettle(page, [`[data-timeline-part-id="${shellID}"]`, `[data-timeline-part-id="${followingID}"]`])
const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
})
const plan = visualPlan(regions, [
{ type: "required", regions: ["shell", "following"] },
{ type: "unique", regions: ["shell", "following"] },
{ type: "stable", regions: ["shell", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 0 },
{ type: "label-stability", regions: "all" },
{ type: "preserve-bottom-anchor" },
{ type: "flow", regions: ["shell", "following"] },
])
await startVisualProbe(page, regions)
await trigger.click()
await expect(trigger).toHaveAttribute("aria-expanded", "true")
await page.waitForTimeout(500)
const expanded = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(testInfo, "shell-expand", expanded, plan)
await startVisualProbe(page, regions)
await trigger.click()
await expect(trigger).toHaveAttribute("aria-expanded", "false")
await page.waitForTimeout(500)
const collapsed = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(testInfo, "shell-collapse", collapsed, plan)
})
test("expands and collapses a completed context group without overlap", async ({ page }, testInfo) => {
const ids = [
"prt_interaction_01_read",
"prt_interaction_02_glob",
"prt_interaction_03_grep",
"prt_interaction_04_list",
]
const group = `[data-timeline-part-ids="${ids.join(",")}"]`
const followingID = "prt_interaction_context_following"
await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([
toolPart(ids[0]!, "read", "completed", { filePath: "src/a.ts" }),
toolPart(ids[1]!, "glob", "completed", { path: ".", pattern: "**/*.ts" }),
toolPart(ids[2]!, "grep", "completed", { path: ".", pattern: "stable" }),
toolPart(ids[3]!, "list", "completed", { path: "src" }),
textPart(followingID, "Following context expansion"),
]),
],
cpuRate: 4,
seedHistory: true,
})
const trigger = page.locator(`${group} [data-slot="collapsible-trigger"]`)
await waitForVisualSettle(page, [group, `[data-timeline-part-id="${followingID}"]`])
for (const [name, expanded] of [
["context-expand", true],
["context-collapse", false],
["context-reexpand", true],
] as const) {
const regions = defineVisualRegions({
context: { selector: group, closest: '[data-timeline-row="AssistantPart"]' },
following: {
selector: `[data-timeline-part-id="${followingID}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
})
await startVisualProbe(page, regions)
await trigger.click()
await expect(trigger).toHaveAttribute("aria-expanded", String(expanded))
await page.waitForTimeout(500)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
name,
trace,
visualPlan(regions, [
{ type: "required", regions: ["context", "following"] },
{ type: "unique", regions: ["context", "following"] },
{ type: "stable", regions: ["context", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 0 },
{ type: "label-stability", regions: "all" },
{ type: "preserve-bottom-anchor" },
{ type: "flow", regions: ["context", "following"] },
]),
)
}
})
test("expands and collapses an edit diff without moving twice", async ({ page }, testInfo) => {
const editID = "prt_interaction_edit"
const followingID = "prt_interaction_edit_following"
await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([
toolPart(
editID,
"edit",
"completed",
{ filePath: "src/edit.ts" },
{
metadata: {
filediff: {
file: "src/edit.ts",
additions: 40,
deletions: 40,
before: source(40, false),
after: source(40, true),
},
},
},
),
textPart(followingID, "Following edit expansion"),
]),
],
settings: { editToolPartsExpanded: false },
cpuRate: 4,
seedHistory: true,
})
const trigger = page.locator(`[data-timeline-part-id="${editID}"] [data-slot="collapsible-trigger"]`).first()
await waitForVisualSettle(page, [`[data-timeline-part-id="${editID}"]`, `[data-timeline-part-id="${followingID}"]`])
const regions = defineVisualRegions({
edit: { selector: `[data-timeline-part-id="${editID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
})
await startVisualProbe(page, regions)
await trigger.click()
await expect(trigger).toHaveAttribute("aria-expanded", "true")
await page.waitForTimeout(900)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"edit-expand",
trace,
visualPlan(regions, [
{ type: "required", regions: ["edit", "following"] },
{ type: "unique", regions: ["edit", "following"] },
{ type: "stable", regions: ["edit", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 0, maxReversals: 1 },
{ type: "label-stability", regions: "all" },
{ type: "preserve-bottom-anchor" },
{ type: "flow", regions: ["edit", "following"] },
]),
)
})
test("shows all and expands historical diff summary without overlap", async ({ page }, testInfo) => {
const firstUser = userMessage(undefined, {
summary: {
diffs: Array.from({ length: 12 }, (_, index) => ({
file: `src/diff-${index}.ts`,
additions: 1,
deletions: 1,
patch: `@@ -1 +1 @@\n-export const value = ${index}\n+export const value = ${index + 1}`,
})),
},
})
const nextUserID = "msg_2000_diff_interaction_user"
await setupTimeline(page, {
messages: [
firstUser,
assistantMessage(),
userMessage(undefined, { id: nextUserID, created: 1700000010000 }),
assistantMessage([], {
id: "msg_2001_diff_interaction_assistant",
parentID: nextUserID,
created: 1700000011000,
}),
],
cpuRate: 4,
})
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
await scroller.evaluate((element) => (element.scrollTop = 0))
const diff = page.locator('[data-timeline-row="DiffSummary"]')
const following = page.locator(`[data-message-id="${nextUserID}"]`).first()
await expect(diff).toBeVisible()
const regions = defineVisualRegions({
diff: { selector: '[data-timeline-row="DiffSummary"]' },
following: { selector: `[data-message-id="${nextUserID}"]` },
})
await startVisualProbe(page, regions)
await page.getByText(/show all/i).click()
await page.waitForTimeout(500)
await diff.locator('[data-slot="session-turn-diff-trigger"]').first().click()
await page.waitForTimeout(900)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"diff-summary-expand",
trace,
visualPlan(regions, [
{ type: "required", regions: ["diff", "following"] },
{ type: "unique", regions: ["diff", "following"] },
{ type: "stable", regions: ["diff", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 1, maxReversals: 2 },
{ type: "label-stability", regions: "all" },
{ type: "flow", regions: ["diff", "following"] },
]),
)
})
function lines(count: number) {
return Array.from({ length: count }, (_, index) => `line ${index + 1}`).join("\n")
}
function source(count: number, changed: boolean) {
return Array.from(
{ length: count },
(_, index) => `export const value${index} = ${changed ? index + 1 : index}\n`,
).join("")
}
@@ -0,0 +1,157 @@
import { expect, test } from "@playwright/test"
import {
defineVisualRegions,
mapVisualRegions,
reportVisualStability,
startVisualProbe,
stopVisualProbe,
visualPlan,
} from "../../utils/visual-stability"
import {
assistantMessage,
completedAssistantInfo,
messageUpdated,
partDelta,
partUpdated,
reasoningPart,
setupTimeline,
shell,
status,
textPart,
userMessage,
waitForVisualSettle,
} from "./fixture"
test.describe("timeline visual lifecycle stability", () => {
test("streams empty, short, and long parallel shells to staggered completion", async ({ page }, testInfo) => {
test.setTimeout(180_000)
const ids = ["prt_parallel_01_empty", "prt_parallel_02_short", "prt_parallel_03_long"] as const
const initial = ids.map((id) => shell(id, "running"))
const followingID = "prt_parallel_04_following"
const assistant = assistantMessage([...initial, textPart(followingID, "Following all parallel shells.")], {
completed: false,
})
const timeline = await setupTimeline(page, {
messages: [userMessage(), assistant],
settings: { shellToolPartsExpanded: true, showReasoningSummaries: true },
cpuRate: 4,
eventRetry: 24,
seedHistory: true,
})
await timeline.send(status("busy"), 150)
for (const id of ids) await timeline.waitForPart(id)
const scroller = page.locator(".scroll-view__viewport", {
has: page.locator('[data-timeline-row="AssistantPart"]'),
})
await scroller.evaluate((element) => (element.scrollTop = element.scrollHeight))
const regions = defineVisualRegions({
prt_shell_empty: shellRegion(ids[0]),
prt_shell_short: shellRegion(ids[1]),
prt_shell_long: shellRegion(ids[2]),
following: shellRegion(followingID),
})
await waitForVisualSettle(page, [`[data-timeline-part-id="${followingID}"]`])
await startVisualProbe(page, regions)
await timeline.sendAll([
{ event: partUpdated(shell(ids[0]!, "completed", "")), delay: 180 },
{ event: partUpdated(shell(ids[2]!, "running", lines(10))), delay: 70 },
{ event: partUpdated(shell(ids[1]!, "running", lines(2))), delay: 110 },
{ event: partUpdated(shell(ids[2]!, "running", lines(25))), delay: 80 },
{ event: partUpdated(shell(ids[1]!, "completed", lines(2))), delay: 260 },
{ event: partUpdated(shell(ids[2]!, "running", lines(50))), delay: 100 },
{ event: partUpdated(shell(ids[2]!, "completed", lines(50))), delay: 450 },
{ event: messageUpdated(completedAssistantInfo(assistant.info)), delay: 100 },
{ event: status("idle"), delay: 700 },
])
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"parallel-shells",
trace,
visualPlan(
regions,
[
{ type: "required", regions: ["prt_shell_empty", "prt_shell_short", "prt_shell_long", "following"] },
{ type: "unique", regions: ["prt_shell_empty", "prt_shell_short", "prt_shell_long"] },
{ type: "stable", regions: ["prt_shell_empty", "prt_shell_short", "prt_shell_long", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 0, maxReversals: 4 },
{ type: "label-stability", regions: "all" },
{ type: "preserve-bottom-anchor" },
{ type: "flow", regions: ["prt_shell_empty", "prt_shell_short", "prt_shell_long", "following"] },
],
{ perMarker: true },
),
)
await expect(page.locator(`[data-timeline-part-id="${ids[2]}"] [data-slot="bash-pre"]`)).toContainText("line 50")
const short = page.locator(`[data-timeline-part-id="${ids[1]}"]`)
await short.locator('[data-slot="collapsible-trigger"]').click()
await expect(short.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "false")
await timeline.send(partUpdated(textPart("prt_late_sibling", "A later sibling rerender.")), 250)
await expect(short.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "false")
})
test("replaces thinking with streamed reasoning and text without a blank visible turn", async ({
page,
}, testInfo) => {
const reasoningID = "prt_reasoning_visible"
const textID = "prt_streamed_text"
const assistant = assistantMessage([], { completed: false })
const timeline = await setupTimeline(page, {
messages: [userMessage(), assistant],
settings: { showReasoningSummaries: true },
cpuRate: 4,
})
await timeline.send(status("busy"), 120)
await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible()
const regions = defineVisualRegions({
thinking: { selector: '[data-timeline-row="Thinking"]' },
reasoning: {
selector: `[data-timeline-part-id="${reasoningID}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
text: { selector: `[data-timeline-part-id="${textID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
})
await startVisualProbe(page, regions)
await timeline.send(partUpdated(reasoningPart(reasoningID, "")), 100)
await expect(page.locator(`[data-timeline-part-id="${reasoningID}"]`)).toHaveCount(0)
await timeline.send(partUpdated(reasoningPart(reasoningID, "## Planning\n\nChecking the visible timeline.")), 160)
await timeline.waitForPart(reasoningID)
await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0)
await timeline.send(partUpdated(textPart(textID, "Starting")), 100)
await timeline.send(partDelta(textID, " **stable"), 90)
await timeline.send(partDelta(textID, " output** with `code` and [a link"), 130)
await timeline.send(partDelta(textID, "](https://example.com)."), 220)
await timeline.send(messageUpdated(completedAssistantInfo(assistant.info)), 120)
await timeline.send(status("idle"), 500)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"reasoning-text-handoff",
trace,
visualPlan(regions, [
{ type: "required", regions: ["reasoning", "text"] },
{ type: "continuous-any", regions: ["thinking", "reasoning", "text"] },
{ type: "unique", regions: ["reasoning", "text"] },
{ type: "stable", regions: ["reasoning", "text"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxReversals: 4 },
{ type: "label-stability", regions: "all" },
{ type: "flow", regions: ["reasoning", "text"] },
]),
)
await expect(page.locator(`[data-timeline-part-id="${textID}"]`)).toContainText("stable output")
})
})
function lines(count: number) {
return Array.from({ length: count }, (_, index) => `line ${index + 1}`).join("\n")
}
function shellRegion(id: string) {
return { selector: `[data-timeline-part-id="${id}"]`, closest: '[data-timeline-row="AssistantPart"]' }
}
@@ -0,0 +1,75 @@
import { expect, test } from "@playwright/test"
import {
analyzeVisualObservations,
defineVisualRegions,
startVisualProbe,
stopVisualProbe,
visualPlan,
} from "../../utils/visual-stability"
import { assistantMessage, setupTimeline, textPart, userMessage } from "./fixture"
test("detects blanking caused by ancestor opacity", async ({ page }) => {
const partID = "prt_oracle_ancestor_opacity"
await setupTimeline(page, { messages: [userMessage(), assistantMessage([textPart(partID, "Visible content")])] })
const row = page.locator(`[data-timeline-part-id="${partID}"]`).first()
const regions = defineVisualRegions({
content: { selector: `[data-timeline-part-id="${partID}"]` },
})
await startVisualProbe(page, regions)
await row.evaluate((element) => {
element.parentElement!.style.opacity = "0"
})
await page.waitForTimeout(50)
await row.evaluate((element) => {
element.parentElement!.style.opacity = "1"
})
await page.waitForTimeout(50)
const trace = await stopVisualProbe<keyof typeof regions>(page)
const issues = analyzeVisualObservations(
trace.samples,
visualPlan(regions, [
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all" },
{ type: "label-stability", regions: "all" },
]),
)
expect(issues.some((issue) => issue.includes("blanked between visible frames"))).toBe(true)
})
test("detects root opacity when probing descendant opacity", async ({ page }) => {
const partID = "prt_oracle_descendant_opacity"
await setupTimeline(page, { messages: [userMessage(), assistantMessage([textPart(partID, "Visible content")])] })
const row = page.locator(`[data-timeline-part-id="${partID}"]`).first()
await row.evaluate((element) => {
element.innerHTML = '<span data-probe-opacity="true">Visible content</span>'
})
const regions = defineVisualRegions({
content: {
selector: `[data-timeline-part-id="${partID}"]`,
opacitySelectors: ['[data-probe-opacity="true"]'],
},
})
await startVisualProbe(page, regions)
await row.evaluate((element) => {
;(element as HTMLElement).style.opacity = "0"
})
await page.waitForTimeout(50)
await row.evaluate((element) => {
;(element as HTMLElement).style.opacity = "1"
})
await page.waitForTimeout(50)
const trace = await stopVisualProbe<keyof typeof regions>(page)
const issues = analyzeVisualObservations(
trace.samples,
visualPlan(regions, [
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all" },
{ type: "label-stability", regions: "all" },
]),
)
expect(issues.some((issue) => issue.includes("blanked between visible frames"))).toBe(true)
})
@@ -0,0 +1,17 @@
import config from "../playwright.config"
export default {
...config,
testDir: ".",
testMatch: "**/*.spec.ts",
outputDir: "../../test-results/timeline-stability",
reporter: [["html", { outputFolder: "../../playwright-report/timeline-stability", open: "never" }], ["line"]],
retries: 0,
workers: 1,
use: {
...config.use,
trace: "retain-on-failure",
screenshot: "only-on-failure",
video: "retain-on-failure",
},
}
@@ -0,0 +1,353 @@
import { expect, test } from "@playwright/test"
import {
defineVisualRegions,
reportVisualStability,
startVisualProbe,
stopVisualProbe,
visualPlan,
} from "../../utils/visual-stability"
import {
assistantMessage,
partUpdated,
setupTimeline,
shell,
textPart,
userMessage,
type TimelineMessage,
} from "./fixture"
test("does not reverse visible rows when the user wheels during shell remeasurement", async ({ page }, testInfo) => {
const shellID = "prt_wheel_01_shell"
const followingID = "prt_wheel_02_following"
const timeline = await setupTimeline(page, {
messages: [
...history(12),
userMessage(),
assistantMessage([shell(shellID, "running"), textPart(followingID, "Following wheel interaction")], {
completed: false,
}),
],
settings: { shellToolPartsExpanded: true },
cpuRate: 4,
reducedMotion: true,
seedHistory: true,
})
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
})
await startVisualProbe(page, regions)
await timeline.send(partUpdated(shell(shellID, "running", lines(30))), 80)
await scroller.evaluate((element) =>
element.dispatchEvent(new WheelEvent("wheel", { bubbles: true, cancelable: true, deltaY: -180 })),
)
await scroller.evaluate((element) => (element.scrollTop -= 180))
await timeline.send(partUpdated(shell(shellID, "running", lines(50))), 250)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(testInfo, "wheel-during-resize", trace, rowPairPlan(regions, 1))
})
test("keeps moving upward while drag-selecting above the timeline", async ({ page }) => {
await setupTimeline(page, {
messages: history(80),
viewport: { width: 1400, height: 700 },
reducedMotion: true,
})
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
const text = page.getByText("History 79.", { exact: false })
await expect(text).toBeVisible()
await scroller.evaluate((element) => {
element.dataset.selectionLength = "0"
document.addEventListener("selectionchange", () => {
element.dataset.selectionLength = String(
Math.max(Number(element.dataset.selectionLength), window.getSelection()?.toString().length ?? 0),
)
})
})
const textBox = await text.boundingBox()
const scrollBox = await scroller.boundingBox()
expect(textBox).not.toBeNull()
expect(scrollBox).not.toBeNull()
if (!textBox || !scrollBox) return
await page.mouse.move(textBox.x + textBox.width - 10, textBox.y + textBox.height / 2)
await page.mouse.down()
await page.mouse.move(textBox.x + 20, scrollBox.y - 120, { steps: 30 })
await expect.poll(() => scroller.evaluate((element) => Number(element.dataset.selectionLength))).toBeGreaterThan(0)
await expect
.poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop))
.toBeGreaterThan(500)
await page.mouse.up()
})
test("does not pull a keyboard-scrolled user during shell remeasurement", async ({ page }, testInfo) => {
const shellID = "prt_keyboard_01_shell"
const followingID = "prt_keyboard_02_following"
const timeline = await setupTimeline(page, {
messages: [
...history(12),
userMessage(),
assistantMessage([shell(shellID, "running"), textPart(followingID, "Following keyboard interaction")], {
completed: false,
}),
],
settings: { shellToolPartsExpanded: true },
cpuRate: 4,
})
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
await scroller.focus()
for (let index = 0; index < 3; index++) {
await scroller.press("PageUp")
await page.waitForTimeout(250)
}
await expect
.poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop), {
timeout: 20_000,
})
.toBeGreaterThan(80)
await page.waitForFunction(() => {
const root = [...document.querySelectorAll<HTMLElement>(".scroll-view__viewport")].find((element) =>
element.querySelector("[data-timeline-row]"),
)
if (!root) return false
return new Promise<boolean>((resolve) => {
const top = root.scrollTop
requestAnimationFrame(() => requestAnimationFrame(() => resolve(Math.abs(root.scrollTop - top) <= 0.5)))
})
})
const anchor = await scroller.evaluate((element) => {
const view = element.getBoundingClientRect()
return [...element.querySelectorAll<HTMLElement>("[data-timeline-key]")].find((row) => {
const rect = row.getBoundingClientRect()
return rect.top >= view.top + 40 && rect.bottom <= view.bottom - 40
})?.dataset.timelineKey
})
expect(anchor).toBeTruthy()
const regions = defineVisualRegions({
anchor: { selector: `[data-timeline-key="${anchor}"]` },
})
await startVisualProbe(page, regions)
await timeline.send(partUpdated(shell(shellID, "running", lines(50))), 400)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(testInfo, "keyboard-during-resize", trace, anchorPlan(regions))
})
test("tracks keyboard scrolling from a focused timeline descendant", async ({ page }, testInfo) => {
const shellID = "prt_descendant_keyboard_01_shell"
const timeline = await setupTimeline(page, {
messages: [...history(12), userMessage(), assistantMessage([shell(shellID, "completed", lines(5))])],
settings: { shellToolPartsExpanded: false },
cpuRate: 4,
reducedMotion: true,
})
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
const row = page.locator(`[data-timeline-part-id="${shellID}"]`).first()
const trigger = page.locator(`[data-timeline-part-id="${shellID}"] [data-slot="collapsible-trigger"]`)
await row.evaluate((element) => element.setAttribute("tabindex", "0"))
await row.focus()
for (let index = 0; index < 3; index++) {
await row.press("PageUp")
await page.waitForTimeout(250)
}
await expect
.poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop))
.toBeGreaterThan(5)
const anchor = await scroller.evaluate((element) => {
const view = element.getBoundingClientRect()
return [...element.querySelectorAll<HTMLElement>("[data-timeline-key]")].find((row) => {
const rect = row.getBoundingClientRect()
return rect.top >= view.top + 40 && rect.bottom <= view.bottom - 40
})?.dataset.timelineKey
})
expect(anchor).toBeTruthy()
const regions = defineVisualRegions({
anchor: { selector: `[data-timeline-key="${anchor}"]` },
})
await startVisualProbe(page, regions)
await trigger.click()
await page.waitForTimeout(300)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(testInfo, "descendant-keyboard-resize", trace, anchorPlan(regions))
})
test("does not claim keyboard scrolling owned by a nested scrollable", async ({ page }) => {
const shellID = "prt_nested_keyboard_shell"
await setupTimeline(page, {
messages: [userMessage(), assistantMessage([shell(shellID, "completed", lines(50))])],
settings: { shellToolPartsExpanded: true },
cpuRate: 4,
reducedMotion: true,
seedHistory: true,
})
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
const nested = page.locator(`[data-timeline-part-id="${shellID}"] [data-scrollable]`)
await nested.evaluate((element) => (element.scrollTop = element.scrollHeight))
await nested.focus()
await page.waitForFunction(() => {
const root = [...document.querySelectorAll<HTMLElement>(".scroll-view__viewport")].find((element) =>
element.querySelector("[data-timeline-row]"),
)
if (!root) return false
return new Promise<boolean>((resolve) => {
const top = root.scrollTop
requestAnimationFrame(() => requestAnimationFrame(() => resolve(Math.abs(root.scrollTop - top) <= 0.5)))
})
})
const before = await scroller.evaluate((element) => element.scrollTop)
const nestedBefore = await nested.evaluate((element) => element.scrollTop)
await nested.press("PageUp")
await page.waitForTimeout(300)
expect(await scroller.evaluate((element) => element.scrollTop)).toBe(before)
expect(await nested.evaluate((element) => element.scrollTop)).toBeLessThan(nestedBefore)
await nested.evaluate((element) => (element.scrollTop = 0))
await scroller.evaluate((element) => (element.scrollTop = Math.min(300, element.scrollHeight - element.clientHeight)))
const boundaryBefore = await scroller.evaluate((element) => element.scrollTop)
expect(boundaryBefore).toBeGreaterThan(0)
await nested.press("PageUp")
await expect.poll(() => scroller.evaluate((element) => element.scrollTop)).toBeLessThan(boundaryBefore)
const nonOverflowing = page.locator(`[data-timeline-part-id="${shellID}"]`).first()
await nonOverflowing.evaluate((element) => {
element.setAttribute("data-scrollable", "")
element.setAttribute("tabindex", "0")
})
await nonOverflowing.focus()
const nonOverflowBefore = await scroller.evaluate((element) => element.scrollTop)
await nonOverflowing.press("PageUp")
await expect.poll(() => scroller.evaluate((element) => element.scrollTop)).toBeLessThan(nonOverflowBefore)
})
test("jump to latest lands on stable final rows after offscreen growth", async ({ page }, testInfo) => {
const shellID = "prt_jump_01_shell"
const followingID = "prt_jump_02_following"
const timeline = await setupTimeline(page, {
messages: [
...history(20),
userMessage(),
assistantMessage([shell(shellID, "running"), textPart(followingID, "Latest visible row")], { completed: false }),
],
settings: { shellToolPartsExpanded: true },
cpuRate: 4,
})
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
await scroller.evaluate(
(element) => (element.scrollTop = Math.max(0, element.scrollHeight - element.clientHeight - 600)),
)
await timeline.send(partUpdated(shell(shellID, "running", lines(50))), 300)
const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
})
await startVisualProbe(page, regions)
await page.getByRole("button", { name: /Jump to latest/i }).click()
await expect(page.locator(`[data-timeline-part-id="${followingID}"]`)).toBeVisible()
await page.waitForTimeout(600)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"jump-latest",
trace,
visualPlan(regions, [
{ type: "required", regions: ["shell", "following"] },
{ type: "unique", regions: ["shell", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 1 },
{ type: "label-stability", regions: "all" },
{ type: "acquire-bottom-anchor" },
{ type: "flow", regions: ["shell", "following"] },
]),
)
})
test("handles a single row taller than the viewport", async ({ page }, testInfo) => {
const shellID = "prt_tall_01_shell"
const followingID = "prt_tall_02_following"
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([shell(shellID, "running"), textPart(followingID, "After tall row")], { completed: false }),
],
settings: { shellToolPartsExpanded: true },
viewport: { width: 900, height: 360 },
cpuRate: 4,
seedHistory: true,
})
const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
})
await startVisualProbe(page, regions)
await timeline.send(partUpdated(shell(shellID, "completed", lines(100))), 700)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"taller-than-viewport",
trace,
visualPlan(regions, [
{ type: "required", regions: ["shell", "following"] },
{ type: "unique", regions: ["shell", "following"] },
{ type: "stable", regions: ["shell", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 0 },
{ type: "label-stability", regions: "all" },
{ type: "preserve-bottom-anchor" },
{ type: "flow", regions: ["shell", "following"] },
]),
)
})
function history(count: number): TimelineMessage[] {
return Array.from({ length: count }, (_, index) => {
const prefix = `msg_${String(index).padStart(4, "0")}_scroll`
const userID = `${prefix}_a_user`
return [
userMessage(undefined, { id: userID, created: 1690000000000 + index * 10_000 }),
assistantMessage(
[textPart(`prt_${String(index).padStart(4, "0")}_scroll`, `History ${index}. ${"content ".repeat(30)}`)],
{
id: `${prefix}_b_assistant`,
parentID: userID,
created: 1690000001000 + index * 10_000,
},
),
]
}).flat()
}
function lines(count: number) {
return Array.from({ length: count }, (_, index) => `line ${index + 1}`).join("\n")
}
function rowPairPlan(
regions: Record<"shell" | "following", { selector: string; closest?: string }>,
maxPositionReversals: number,
) {
return visualPlan(regions, [
{ type: "required", regions: ["shell", "following"] },
{ type: "unique", regions: ["shell", "following"] },
{ type: "stable", regions: ["shell", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals },
{ type: "label-stability", regions: "all" },
{ type: "flow", regions: ["shell", "following"] },
])
}
function anchorPlan(regions: Record<"anchor", { selector: string; closest?: string }>) {
return visualPlan(regions, [
{ type: "required", regions: ["anchor"] },
{ type: "unique", regions: ["anchor"] },
{ type: "stable", regions: ["anchor"] },
{ type: "fixed", regions: ["anchor"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 0 },
{ type: "label-stability", regions: "all" },
])
}
@@ -0,0 +1,255 @@
import { test } from "@playwright/test"
import {
defineVisualRegions,
reportVisualStability,
startVisualProbe,
stopVisualProbe,
visualPlan,
} from "../../utils/visual-stability"
import {
assistantMessage,
partUpdated,
setupTimeline,
shell,
textPart,
userMessage,
waitForVisualSettle,
} from "./fixture"
const profiles = [
{
name: "empty running to completed",
updates: [{ state: "completed" as const, output: "", delay: 350 }],
},
{
name: "50 lines arriving incrementally",
updates: [
{ state: "running" as const, output: lines(1), delay: 100 },
{ state: "running" as const, output: lines(10), delay: 160 },
{ state: "running" as const, output: lines(25), delay: 90 },
{ state: "running" as const, output: lines(50), delay: 220 },
{ state: "completed" as const, output: lines(50), delay: 500 },
],
},
{
name: "wide ANSI and CRLF output",
updates: [
{
state: "running" as const,
output: Array.from({ length: 20 }, (_, index) => `\u001b[32mline ${index}\u001b[0m ${"wide-".repeat(30)}`).join(
"\r\n",
),
delay: 240,
},
{
state: "completed" as const,
output: Array.from({ length: 20 }, (_, index) => `line ${index} ${"wide-".repeat(30)}`).join("\n"),
delay: 500,
},
],
},
] as const
for (const profile of profiles) {
test(`keeps rows stable for shell ${profile.name}`, async ({ page }, testInfo) => {
const shellID = `prt_matrix_${profiles.indexOf(profile)}_01_shell`
const followingID = `prt_matrix_${profiles.indexOf(profile)}_02_following`
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([shell(shellID, "running"), textPart(followingID, "Following shell row")], {
completed: false,
}),
],
settings: { shellToolPartsExpanded: true },
cpuRate: 4,
seedHistory: true,
})
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
await scroller.evaluate((element) => (element.scrollTop = element.scrollHeight))
await waitForVisualSettle(page, [
`[data-timeline-part-id="${shellID}"]`,
`[data-timeline-part-id="${followingID}"]`,
])
const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
following: {
selector: `[data-timeline-part-id="${followingID}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
})
await startVisualProbe(page, regions)
for (const update of profile.updates) {
await timeline.send(partUpdated(shell(shellID, update.state, update.output)), update.delay)
}
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
`shell-${profiles.indexOf(profile)}`,
trace,
visualPlan(
regions,
[
{ type: "required", regions: ["shell", "following"] },
{ type: "unique", regions: ["shell", "following"] },
{ type: "stable", regions: ["shell", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 0, maxReversals: 1 },
{ type: "label-stability", regions: "all" },
{ type: "preserve-bottom-anchor" },
{ type: "flow", regions: ["shell", "following"] },
],
{ perMarker: true },
),
)
})
}
test("keeps following row stable when a collapsed shell receives 50 lines", async ({ page }, testInfo) => {
const shellID = "prt_matrix_collapsed_01_shell"
const followingID = "prt_matrix_collapsed_02_following"
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([shell(shellID, "running"), textPart(followingID, "Following collapsed shell")], {
completed: false,
}),
],
settings: { shellToolPartsExpanded: false },
cpuRate: 4,
seedHistory: true,
})
await waitForVisualSettle(page, [`[data-timeline-part-id="${shellID}"]`, `[data-timeline-part-id="${followingID}"]`])
const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
})
await startVisualProbe(page, regions)
await timeline.send(partUpdated(shell(shellID, "running", lines(50))), 240)
await timeline.send(partUpdated(shell(shellID, "completed", lines(50))), 500)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"collapsed-shell",
trace,
visualPlan(
regions,
[
{ type: "required", regions: ["shell", "following"] },
{ type: "unique", regions: ["shell", "following"] },
{ type: "stable", regions: ["shell", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: ["following"], maxPositionReversals: 0 },
{ type: "label-stability", regions: "all" },
{ type: "flow", regions: ["shell", "following"] },
],
{ perMarker: true },
),
)
})
test("keeps rows stable when a running shell becomes an error", async ({ page }, testInfo) => {
const shellID = "prt_matrix_error_01_shell"
const followingID = "prt_matrix_error_02_following"
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([shell(shellID, "running", lines(10)), textPart(followingID, "Following failed shell")], {
completed: false,
}),
],
settings: { shellToolPartsExpanded: true },
cpuRate: 4,
seedHistory: true,
})
await waitForVisualSettle(page, [`[data-timeline-part-id="${shellID}"]`, `[data-timeline-part-id="${followingID}"]`])
const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
})
await startVisualProbe(page, regions)
await timeline.send(
partUpdated({
...shell(shellID, "error"),
state: {
status: "error",
input: { command: `echo ${shellID}` },
error: "Command failed after output",
metadata: {},
time: { start: 1700000001000, end: 1700000002000 },
},
}),
500,
)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"shell-error",
trace,
visualPlan(
regions,
[
{ type: "required", regions: ["shell", "following"] },
{ type: "unique", regions: ["shell", "following"] },
{ type: "stable", regions: ["shell", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: ["following"], maxPositionReversals: 0 },
{ type: "label-stability", regions: "all" },
{ type: "preserve-bottom-anchor" },
{ type: "flow", regions: ["shell", "following"] },
],
{ perMarker: true },
),
)
})
test("keeps rows stable when later text arrives before shell output", async ({ page }, testInfo) => {
const shellID = "prt_late_text_01_shell"
const followingID = "prt_late_text_02_following"
const timeline = await setupTimeline(page, {
messages: [userMessage(), assistantMessage([shell(shellID, "running")], { completed: false })],
settings: { shellToolPartsExpanded: true },
cpuRate: 4,
seedHistory: true,
})
await waitForVisualSettle(page, [`[data-timeline-part-id="${shellID}"]`])
const regions = defineVisualRegions({
shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
following: {
selector: `[data-timeline-part-id="${followingID}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
})
await startVisualProbe(page, regions)
await timeline.send(partUpdated(textPart(followingID, "Later assistant content arrived before shell output.")), 240)
await timeline.send(partUpdated(shell(shellID, "running", lines(20))), 300)
await timeline.send(partUpdated(shell(shellID, "completed", lines(20))), 600)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"late-text-before-shell-output",
trace,
visualPlan(
regions,
[
{ type: "required", regions: ["shell", "following"] },
{ type: "unique", regions: ["shell", "following"] },
{ type: "stable", regions: ["shell"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 0 },
{ type: "label-stability", regions: "all" },
{ type: "preserve-bottom-anchor" },
{ type: "flow", regions: ["shell", "following"] },
],
{ perMarker: true },
),
)
})
function lines(count: number) {
return Array.from({ length: count }, (_, index) => `line ${index + 1}`).join("\n")
}
@@ -0,0 +1,106 @@
import { expect, test } from "@playwright/test"
import {
defineVisualRegions,
reportVisualStability,
startVisualProbe,
stopVisualProbe,
visualPlan,
} from "../../utils/visual-stability"
import {
assistantMessage,
partUpdated,
session,
sessionID,
setupTimeline,
textPart,
toolPart,
userMessage,
} from "./fixture"
test("adds a task child-session link without replacing the task row", async ({ page }, testInfo) => {
const taskID = "prt_task_link"
const childID = "ses_task_child"
const input = { description: "Inspect child", subagent_type: "explore" }
const timeline = await setupTimeline(page, {
messages: [userMessage(), assistantMessage([toolPart(taskID, "task", "running", input)], { completed: false })],
sessions: [session(), session({ id: childID, parentID: sessionID, title: "Inspect child" })],
cpuRate: 4,
})
const regions = defineVisualRegions({
task: { selector: `[data-timeline-part-id="${taskID}"] [data-slot="collapsible-trigger"]` },
})
await startVisualProbe(page, regions)
await timeline.send(
partUpdated(toolPart(taskID, "task", "completed", input, { metadata: { sessionId: childID } })),
500,
)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"task-link",
trace,
visualPlan(regions, [
{ type: "required", regions: ["task"] },
{ type: "unique", regions: ["task"] },
{ type: "stable", regions: ["task"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 0 },
{ type: "label-stability", regions: "all" },
]),
)
await expect(
page.locator(`a[href$="/session/${childID}"]`, { has: page.locator('[data-component="task-tool-card"]') }),
).toBeVisible()
})
test("changes generic tool arguments without replacing the row", async ({ page }, testInfo) => {
const toolID = "prt_generic_mutation"
const followingID = "prt_generic_mutation_following"
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage(
[
toolPart(toolID, "mcp_probe", "running", { target: "one", count: 1 }),
textPart(followingID, "Following generic tool"),
],
{ completed: false },
),
],
cpuRate: 4,
})
const regions = defineVisualRegions({
tool: { selector: `[data-timeline-part-id="${toolID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
})
await startVisualProbe(page, regions)
await timeline.send(
partUpdated(toolPart(toolID, "mcp_probe", "running", { target: "two", count: 2, mode: "deep" })),
200,
)
await timeline.send(
partUpdated(toolPart(toolID, "mcp_probe", "completed", { target: "two", count: 2, mode: "deep" })),
400,
)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"generic-mutation",
trace,
visualPlan(
regions,
[
{ type: "required", regions: ["tool", "following"] },
{ type: "unique", regions: ["tool", "following"] },
{ type: "stable", regions: ["tool", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 0 },
{ type: "label-stability", regions: "all" },
{ type: "flow", regions: ["tool", "following"] },
],
{ perMarker: true },
),
)
})
@@ -0,0 +1,198 @@
import { expect, test } from "@playwright/test"
import {
defineVisualRegions,
reportVisualStability,
startVisualProbe,
stopVisualProbe,
visualPlan,
} from "../../utils/visual-stability"
import {
assistantMessage,
directory,
partUpdated,
session,
sessionID,
setupTimeline,
status,
textPart,
toolPart,
userMessage,
} from "./fixture"
test.describe("timeline tool state stability", () => {
test("moves lightweight tools through pending, running, and completed without replacing rows", async ({
page,
}, testInfo) => {
const ids = ["webfetch", "websearch", "task", "skill", "custom"] as const
const inputs = {
webfetch: { url: "https://example.com/docs" },
websearch: { query: "timeline stability" },
task: { description: "Inspect timeline", subagent_type: "explore" },
skill: { name: "stability" },
custom: { target: "timeline", depth: 2 },
}
const names = { webfetch: "webfetch", websearch: "websearch", task: "task", skill: "skill", custom: "mcp_probe" }
const questionID = "prt_state_question"
const todoID = "prt_state_todo"
const initial = [
...ids.map((id) => toolPart(`prt_state_${id}`, names[id], "pending", inputs[id])),
toolPart(questionID, "question", "pending", questionInput()),
toolPart(todoID, "todowrite", "pending", { todos: [{ content: "Hidden", status: "pending" }] }),
textPart("prt_state_following", "Following lightweight tools"),
]
const childID = "ses_timeline_child"
const timeline = await setupTimeline(page, {
messages: [userMessage(), assistantMessage(initial, { completed: false })],
sessions: [session(), session({ id: childID, parentID: sessionID, title: "Inspect timeline" })],
cpuRate: 4,
})
await timeline.send(status("busy"), 120)
for (const id of ids) await timeline.waitForPart(`prt_state_${id}`)
await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toHaveCount(0)
await expect(page.locator(`[data-timeline-part-id="${todoID}"]`)).toHaveCount(0)
const regionIDs = [
"prt_state_webfetch",
"prt_state_websearch",
"prt_state_task",
"prt_state_skill",
"prt_state_custom",
] as const
const regions = defineVisualRegions({
prt_state_webfetch: toolRegion(regionIDs[0]),
prt_state_websearch: toolRegion(regionIDs[1]),
prt_state_task: toolRegion(regionIDs[2]),
prt_state_skill: toolRegion(regionIDs[3]),
prt_state_custom: toolRegion(regionIDs[4]),
})
await startVisualProbe(page, regions)
for (const [index, id] of ids.entries()) {
await timeline.send(
partUpdated(toolPart(`prt_state_${id}`, names[id], "running", inputs[id])),
[80, 240, 100, 360, 140][index],
)
}
for (const [index, id] of ["skill", "webfetch", "custom", "task", "websearch"].entries()) {
const key = id as (typeof ids)[number]
const metadata = key === "task" ? { sessionId: childID } : key === "websearch" ? { provider: "exa" } : {}
const output = key === "websearch" ? "Result https://example.com/result" : "Completed"
await timeline.send(
partUpdated(toolPart(`prt_state_${key}`, names[key], "completed", inputs[key], { metadata, output })),
[110, 70, 280, 130, 420][index],
)
}
await timeline.send(
partUpdated(
toolPart(questionID, "question", "completed", questionInput(), { metadata: { answers: [["Keep it stable"]] } }),
),
350,
)
await timeline.waitForPart(questionID)
await timeline.send(status("idle"), 500)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"lightweight-tools",
trace,
visualPlan(regions, [
{ type: "required", regions: regionIDs },
{ type: "unique", regions: regionIDs },
{ type: "stable", regions: regionIDs },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxReversals: 4 },
{ type: "label-stability", regions: "all" },
]),
)
await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toContainText("Keep it stable")
await expect(page.locator(`[data-timeline-part-id="${todoID}"]`)).toHaveCount(0)
await expect(
page.locator(`a[href$="/session/${childID}"]`, { has: page.locator('[data-component="task-tool-card"]') }),
).toBeVisible()
await expect(page.getByRole("button", { name: /Exa Web Search/ })).toBeVisible()
})
test("keeps an expanded mixed context group stable through staggered completion and error", async ({
page,
}, testInfo) => {
const ids = ["prt_ctx_01_read", "prt_ctx_02_glob", "prt_ctx_03_grep", "prt_ctx_04_list"]
const tools = ["read", "glob", "grep", "list"]
const inputs = [
{ filePath: "src/a.ts", offset: 0, limit: 120 },
{ path: directory, pattern: "**/*.ts" },
{ path: directory, pattern: "stability", include: "*.ts" },
{ path: "src" },
]
const context = ids.map((id, index) => toolPart(id, tools[index]!, "pending", inputs[index]!))
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([...context, textPart("prt_ctx_following", "Following context")], { completed: false }),
],
cpuRate: 4,
})
await timeline.send(status("busy"), 100)
const groupSelector = `[data-timeline-part-ids="${ids.join(",")}"]`
const group = page.locator(groupSelector)
await expect(group).toBeVisible()
await group.locator('[data-slot="collapsible-trigger"]').click()
await expect(group.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "true")
const regions = defineVisualRegions({
status: {
selector: `${groupSelector} [data-component="tool-status-title"]`,
opacitySelectors: ['[data-slot="tool-status-active"]', '[data-slot="tool-status-done"]'],
},
context: { selector: groupSelector, closest: '[data-timeline-row="AssistantPart"]' },
following: {
selector: '[data-timeline-part-id="prt_ctx_following"]',
closest: '[data-timeline-row="AssistantPart"]',
},
})
await startVisualProbe(page, regions)
for (const [index, delay] of [90, 260, 70, 380].entries()) {
await timeline.send(partUpdated(toolPart(ids[index]!, tools[index]!, "running", inputs[index]!)), delay)
}
await timeline.send(partUpdated(toolPart(ids[1]!, tools[1]!, "completed", inputs[1]!)), 130)
await timeline.send(partUpdated(toolPart(ids[3]!, tools[3]!, "completed", inputs[3]!)), 210)
await timeline.send(
partUpdated(toolPart(ids[0]!, tools[0]!, "error", inputs[0]!, { error: "Read interrupted" })),
110,
)
await timeline.send(partUpdated(toolPart(ids[2]!, tools[2]!, "completed", inputs[2]!)), 250)
await expect(group.locator('[data-component="tool-status-title"]')).toHaveAttribute("aria-label", "Explored")
await timeline.send(status("idle"), 700)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"mixed-context",
trace,
visualPlan(regions, [
{ type: "required", regions: ["context", "following"] },
{ type: "unique", regions: ["context"] },
{ type: "stable", regions: ["context"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxReversals: 4 },
{ type: "label-stability", regions: "all" },
{ type: "flow", regions: ["context", "following"] },
]),
)
await expect(group.locator('[data-component="tool-status-title"]')).toHaveAttribute("aria-label", "Explored")
await expect(group.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "true")
await group.locator('[data-slot="collapsible-trigger"]').click()
await expect(group.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "false")
await timeline.send(partUpdated(textPart("prt_ctx_late_sibling", "Later sibling content")), 200)
await expect(group.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "false")
await group.locator('[data-slot="collapsible-trigger"]').click()
await expect(group.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "true")
})
})
function questionInput() {
return { questions: [{ header: "Stability", question: "Keep it stable?", options: [] }] }
}
function toolRegion(id: string) {
return { selector: `[data-timeline-part-id="${id}"]`, closest: '[data-timeline-row="AssistantPart"]' }
}
@@ -0,0 +1,272 @@
import { expect, test } from "@playwright/test"
import {
defineVisualRegions,
reportVisualStability,
startVisualProbe,
stopVisualProbe,
visualPlan,
} from "../../utils/visual-stability"
import {
assistantID,
assistantMessage,
completedAssistantInfo,
event,
messageUpdated,
partDelta,
partUpdated,
setupTimeline,
shell,
status,
textPart,
toolPart,
userMessage,
} from "./fixture"
test("keeps unchanged siblings stable while a middle part is inserted and removed", async ({ page }, testInfo) => {
const firstID = "prt_mutation_01_first"
const middleID = "prt_mutation_02_middle"
const lastID = "prt_mutation_03_last"
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([textPart(firstID, "First stable row"), textPart(lastID, "Last stable row")], {
completed: false,
}),
],
cpuRate: 4,
})
const regions = defineVisualRegions({
first: { selector: `[data-timeline-part-id="${firstID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
last: { selector: `[data-timeline-part-id="${lastID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
})
await startVisualProbe(page, regions)
await timeline.send(partUpdated(textPart(middleID, "Inserted middle row. ".repeat(12))), 350)
await expect(page.locator(`[data-timeline-part-id="${middleID}"]`)).toBeVisible()
await timeline.send(
event("message.part.removed", { sessionID: "ses_timeline_stability", messageID: assistantID, partID: middleID }),
500,
)
await expect(page.locator(`[data-timeline-part-id="${middleID}"]`)).toHaveCount(0)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(testInfo, "middle-insert-remove", trace, stablePairPlan(regions, 1))
})
test("streams text through growth, canonical replacement, and completion", async ({ page }, testInfo) => {
const textID = "prt_text_reconcile"
const followingID = "prt_text_reconcile_following"
const assistant = assistantMessage([textPart(textID, "Starting"), textPart(followingID, "Following text row")], {
completed: false,
})
const timeline = await setupTimeline(page, { messages: [userMessage(), assistant], cpuRate: 4 })
const regions = defineVisualRegions({
text: { selector: `[data-timeline-part-id="${textID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
})
await startVisualProbe(page, regions)
await timeline.send(partDelta(textID, " streamed content"), 100)
await timeline.send(partDelta(textID, "\n\n- item one\n- item two\n- item three"), 180)
await timeline.send(partUpdated(textPart(textID, "Canonical replacement with a shorter final paragraph.")), 200)
await timeline.send(messageUpdated(completedAssistantInfo(assistant.info)), 500)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"text-reconcile",
trace,
visualPlan(regions, [
{ type: "required", regions: ["text", "following"] },
{ type: "unique", regions: ["text", "following"] },
{ type: "stable", regions: ["text", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 1, maxReversals: 2 },
{ type: "label-stability", regions: "all" },
{ type: "preserve-bottom-anchor" },
{ type: "flow", regions: ["text", "following"] },
]),
)
})
test("inserts a completed question between stable rows", async ({ page }, testInfo) => {
const firstID = "prt_question_01_first"
const questionID = "prt_question_02_hidden"
const lastID = "prt_question_03_last"
const input = { questions: [{ header: "Choice", question: "Keep stable?", options: [] }] }
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage(
[
textPart(firstID, "Before question"),
toolPart(questionID, "question", "running", input),
textPart(lastID, "After question"),
],
{ completed: false },
),
],
cpuRate: 4,
})
await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toHaveCount(0)
const regions = defineVisualRegions({
first: { selector: `[data-timeline-part-id="${firstID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
last: { selector: `[data-timeline-part-id="${lastID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
})
await startVisualProbe(page, regions)
await timeline.send(
partUpdated(toolPart(questionID, "question", "completed", input, { metadata: { answers: [["Yes"]] } })),
600,
)
await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toBeVisible()
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(testInfo, "question-insert", trace, stablePairPlan(regions, 0))
})
test("replaces thinking with an assistant error without a blank turn", async ({ page }, testInfo) => {
const assistant = assistantMessage([], { completed: false })
const timeline = await setupTimeline(page, { messages: [userMessage(), assistant], cpuRate: 4 })
await timeline.send(status("busy"), 150)
await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible()
const regions = defineVisualRegions({
thinking: { selector: '[data-timeline-row="Thinking"]' },
error: { selector: '[data-timeline-row="Error"]' },
})
await startVisualProbe(page, regions)
await timeline.send(
messageUpdated({
...assistant.info,
error: { name: "APIError", data: { message: "Provider failed visibly", isRetryable: false } },
}),
500,
)
await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0)
await expect(page.locator('[data-timeline-row="Error"]')).toContainText("Provider failed visibly")
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"thinking-error",
trace,
visualPlan(regions, [
{ type: "required", regions: ["thinking", "error"] },
{ type: "continuous-any", regions: ["thinking", "error"] },
{ type: "unique", regions: ["thinking", "error"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all" },
{ type: "label-stability", regions: "all" },
]),
)
})
test("updates retry attempts and long provider messages without remounting the retry row", async ({
page,
}, testInfo) => {
const timeline = await setupTimeline(page, {
messages: [userMessage(), assistantMessage([], { completed: false })],
cpuRate: 4,
})
await timeline.send(status("retry", 1), 120)
await expect(page.locator('[data-timeline-row="Retry"]')).toBeVisible()
const regions = defineVisualRegions({
retry: { selector: '[data-timeline-row="Retry"]' },
})
await startVisualProbe(page, regions)
await timeline.send(
event("session.status", {
sessionID: "ses_timeline_stability",
status: {
type: "retry",
attempt: 2,
message: "A very long provider retry message ".repeat(8),
next: Date.now() + 10_000,
},
}),
300,
)
await timeline.send(status("retry", 3), 300)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"retry-evolution",
trace,
visualPlan(
regions,
[
{ type: "required", regions: ["retry"] },
{ type: "unique", regions: ["retry"] },
{ type: "stable", regions: ["retry"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 0 },
{ type: "label-stability", regions: "all" },
],
{ perMarker: true },
),
)
})
test("reducer-hardening: removes a historical turn one message at a time without moving a visible lower anchor twice", async ({
page,
}, testInfo) => {
const removeUserID = "msg_0500_remove_user"
const removeAssistantID = "msg_0501_remove_assistant"
const anchorUserID = "msg_2000_anchor_user"
const timeline = await setupTimeline(page, {
messages: [
userMessage(undefined, { id: removeUserID, created: 1690000000000 }),
assistantMessage([textPart("prt_remove_text", "Removed historical content. ".repeat(15))], {
id: removeAssistantID,
parentID: removeUserID,
created: 1690000001000,
}),
userMessage(undefined, { id: anchorUserID, created: 1700000000000 }),
assistantMessage([textPart("prt_anchor_text", "Visible anchor response")], {
id: "msg_2001_anchor_assistant",
parentID: anchorUserID,
created: 1700000001000,
}),
],
cpuRate: 4,
})
const regions = defineVisualRegions({
anchor: { selector: `[data-timeline-row="UserMessage"][data-message-id="${anchorUserID}"]` },
})
await startVisualProbe(page, regions)
await timeline.send(
event("message.removed", { sessionID: "ses_timeline_stability", messageID: removeAssistantID }),
200,
)
await timeline.send(event("message.removed", { sessionID: "ses_timeline_stability", messageID: removeUserID }), 500)
const trace = await stopVisualProbe<keyof typeof regions>(page)
await reportVisualStability(
testInfo,
"historical-turn-remove",
trace,
visualPlan(
regions,
[
{ type: "required", regions: ["anchor"] },
{ type: "unique", regions: ["anchor"] },
{ type: "stable", regions: ["anchor"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals: 0 },
{ type: "label-stability", regions: "all" },
],
{ perMarker: true },
),
)
})
function stablePairPlan(
regions: Record<"first" | "last", { selector: string; closest?: string }>,
maxPositionReversals: number,
) {
return visualPlan(regions, [
{ type: "required", regions: ["first", "last"] },
{ type: "unique", regions: ["first", "last"] },
{ type: "stable", regions: ["first", "last"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all", maxPositionReversals },
{ type: "label-stability", regions: "all" },
])
}
@@ -0,0 +1,312 @@
import type { Page } from "@playwright/test"
import { benchmark, expect } from "../benchmark"
import { setupTimelineBenchmark } from "./session-timeline-benchmark.fixture"
const changedLinesPerFile = 100
const linesPerSide = changedLinesPerFile / 2
const fileCounts = [1, 10, 100, 1_000, 10_000]
const filesPerDirectory = 100
const readyFrames = 3
const completionTimeoutMs = Number(process.env.REVIEW_PANE_COMPLETION_TIMEOUT_MS ?? 900_000)
type ReviewPaneScalingSample = {
observedAtMs: number
logicalRows: number
treeRows: number
fileRows: number
diffLines: number
header: string
ready: boolean
}
type ReviewPaneScalingProbe = {
startedAt?: number
firstTreeRowMs?: number
logicalTreeReadyMs?: number
firstDiffRenderMs?: number
stableReadyMs?: number
samples: ReviewPaneScalingSample[]
frameTimesMs: number[]
longTasks: { startTime: number; duration: number }[]
stop: () => void
}
benchmark.describe("performance: review pane scaling", () => {
for (const fileCount of fileCounts) {
const changedLines = fileCount * changedLinesPerFile
benchmark(
`${changedLines} changed lines across ${fileCount} ${fileCount === 1 ? "file" : "files"}`,
async ({ page, report }) => {
benchmark.setTimeout(1_200_000)
await page.emulateMedia({ reducedMotion: "reduce" })
const patchByteLimit = Number(process.env.REVIEW_PANE_PATCH_BYTE_LIMIT ?? Number.POSITIVE_INFINITY)
if (Number.isNaN(patchByteLimit) || patchByteLimit < 0)
throw new Error(`Invalid REVIEW_PANE_PATCH_BYTE_LIMIT: ${process.env.REVIEW_PANE_PATCH_BYTE_LIMIT}`)
const responseBody = JSON.stringify(createScalingDiffs(fileCount, patchByteLimit))
await setupTimelineBenchmark(page, {
historyTurns: 0,
eventBatch: 1,
newLayoutDesigns: true,
})
await page.route("**/vcs/diff**", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
headers: { "access-control-allow-origin": "*" },
body: responseBody,
}),
)
const expectedRows = fileCount + 2 + Math.ceil(fileCount / filesPerDirectory)
const metrics = await measureReviewPaneLoad(page, {
expectedFile: reviewFile(0),
expectedRows,
})
const search = await measureBroadReviewSearch(page, fileCount)
expect(metrics.logicalRows).toBe(expectedRows)
expect(metrics.fileRows).toBeGreaterThan(0)
expect(metrics.treeRows).toBeGreaterThan(0)
expect(metrics.diffLines).toBeGreaterThan(0)
expect(search.logicalRows).toBe(fileCount)
expect(search.renderedRows).toBeGreaterThan(0)
report(
{ ...metrics, search },
{
fileCount,
changedLinesPerFile,
changedLines,
additions: changedLines / 2,
deletions: changedLines / 2,
patchLines: changedLines,
patchByteLimit: Number.isFinite(patchByteLimit) ? patchByteLimit : null,
payloadBytes: new TextEncoder().encode(responseBody).byteLength,
expectedRows,
},
)
},
)
}
})
async function measureBroadReviewSearch(page: Page, expectedRows: number) {
const filter = page.getByRole("searchbox", { name: "Filter files" })
await filter.evaluate((element) => {
element.addEventListener(
"input",
() => {
;(window as Window & { __reviewSearchStartedAt?: number }).__reviewSearchStartedAt = performance.now()
},
{ once: true, capture: true },
)
})
await filter.fill("file-")
return page.evaluate((expectedRows) => {
const startedAt = (window as Window & { __reviewSearchStartedAt?: number }).__reviewSearchStartedAt!
return new Promise<{ stableMs: number; logicalRows: number; renderedRows: number }>((resolve) => {
let previous = -1
let streak = 0
const sample = () => {
const tree = document.querySelector<HTMLElement>('#review-panel [data-component="file-tree-v2"]')
const rows = [...document.querySelectorAll<HTMLElement>('#review-panel [data-slot="file-tree-v2-row"]')]
const logicalRows = Number(tree?.dataset.totalRows ?? rows.length)
const ready =
logicalRows === expectedRows && rows.length > 0 && rows.every((row) => row.textContent?.includes("file-"))
streak = ready && rows.length === previous ? streak + 1 : ready ? 1 : 0
previous = rows.length
if (streak >= 3) {
resolve({ stableMs: performance.now() - startedAt, logicalRows, renderedRows: rows.length })
return
}
requestAnimationFrame(sample)
}
requestAnimationFrame(sample)
})
}, expectedRows)
}
function createScalingDiffs(fileCount: number, patchByteLimit: number) {
const changes = Array.from({ length: linesPerSide }, (_, index) => {
const line = String(index).padStart(3, "0")
return `-export const value_${line} = "before"\n+export const value_${line} = "after"`
}).join("\n")
let patchBytes = 0
let capped = false
return Array.from({ length: fileCount }, (_, index) => {
const file = reviewFile(index)
const fullPatch = [
`diff --git a/${file} b/${file}`,
`--- a/${file}`,
`+++ b/${file}`,
`@@ -1,${linesPerSide} +1,${linesPerSide} @@`,
changes,
].join("\n")
if (index === 0 && fullPatch.length > patchByteLimit)
throw new Error(`REVIEW_PANE_PATCH_BYTE_LIMIT must include the active patch (${fullPatch.length} bytes)`)
const patch = !capped && patchBytes + fullPatch.length <= patchByteLimit ? fullPatch : emptyReviewPatch(file)
if (patch === fullPatch) patchBytes += fullPatch.length
else capped = true
return {
file,
patch,
additions: linesPerSide,
deletions: linesPerSide,
status: "modified" as const,
}
})
}
function emptyReviewPatch(file: string) {
return [`diff --git a/${file} b/${file}`, `--- a/${file}`, `+++ b/${file}`].join("\n")
}
function reviewFile(index: number) {
return `src/review/d${String(Math.floor(index / filesPerDirectory)).padStart(5, "0")}/file-${String(index).padStart(5, "0")}.ts`
}
async function measureReviewPaneLoad(page: Page, input: { expectedFile: string; expectedRows: number }) {
const toggle = page.getByRole("button", { name: "Toggle review" })
await expect(toggle).toBeVisible()
await toggle.evaluate((element) => element.setAttribute("data-review-pane-scaling-toggle", ""))
await installReviewPaneScalingProbe(page, input)
await toggle.click()
await page.waitForFunction(
() =>
(window as Window & { __reviewPaneScalingProbe?: ReviewPaneScalingProbe }).__reviewPaneScalingProbe
?.stableReadyMs !== undefined,
undefined,
{ timeout: completionTimeoutMs },
)
return page.evaluate(() => {
const probe = (window as Window & { __reviewPaneScalingProbe?: ReviewPaneScalingProbe }).__reviewPaneScalingProbe!
probe.stop()
const startedAt = probe.startedAt!
const final = probe.samples.at(-1)!
const resources = performance
.getEntriesByType("resource")
.filter((entry) => entry.name.includes("/vcs/diff")) as PerformanceResourceTiming[]
const resource = resources.at(-1)
const longTasks = probe.longTasks.filter(
(entry) => entry.startTime >= startedAt && entry.startTime <= startedAt + probe.stableReadyMs!,
)
const frameGaps = probe.frameTimesMs.map((time, index) => time - (probe.frameTimesMs[index - 1] ?? 0))
return {
firstTreeRowMs: probe.firstTreeRowMs ?? null,
logicalTreeReadyMs: probe.logicalTreeReadyMs ?? null,
firstDiffRenderMs: probe.firstDiffRenderMs ?? null,
stableReadyMs: probe.stableReadyMs ?? null,
responseStartMs: resource ? resource.responseStart - startedAt : null,
responseEndMs: resource ? resource.responseEnd - startedAt : null,
responseToStableMs: resource ? probe.stableReadyMs! - (resource.responseEnd - startedAt) : null,
treeRows: final.treeRows,
logicalRows: final.logicalRows,
fileRows: final.fileRows,
diffLines: final.diffLines,
samples: probe.samples.length,
maxFrameGapMs: Math.max(0, ...frameGaps),
longTaskCount: longTasks.length,
longTaskTotalMs: longTasks.reduce((sum, entry) => sum + entry.duration, 0),
maxLongTaskMs: Math.max(0, ...longTasks.map((entry) => entry.duration)),
}
})
}
async function installReviewPaneScalingProbe(page: Page, input: { expectedFile: string; expectedRows: number }) {
await page.evaluate(
({ expectedFile, expectedRows, stableFrames }) => {
let running = true
let readyStreak = 0
const basename = expectedFile.split("/").at(-1)!
const longTaskObserver = PerformanceObserver.supportedEntryTypes.includes("longtask")
? new PerformanceObserver((list) => {
probe.longTasks.push(
...list.getEntries().map((entry) => ({ startTime: entry.startTime, duration: entry.duration })),
)
})
: undefined
const probe: ReviewPaneScalingProbe = {
samples: [],
frameTimesMs: [],
longTasks: [],
stop: () => {
running = false
longTaskObserver?.disconnect()
},
}
const sample = (time: number) => {
if (!running || probe.startedAt === undefined) return
const panel = document.querySelector<HTMLElement>("#review-panel")
const tree = panel?.querySelector<HTMLElement>('[data-component="file-tree-v2"]')
const rows = panel?.querySelectorAll('[data-slot="file-tree-v2-row"]') ?? []
const fileRows = panel?.querySelectorAll('button[data-slot="file-tree-v2-row"]') ?? []
const header =
panel?.querySelector<HTMLElement>('[data-slot="session-review-v2-file-header"]')?.textContent?.trim() ?? ""
const viewers = panel
? [...panel.querySelectorAll<HTMLElement>('[data-component="file"][data-mode="diff"]')]
: []
const diffLines = viewers.reduce(
(sum, viewer) =>
sum + (viewer.querySelector("diffs-container")?.shadowRoot?.querySelectorAll("[data-line]").length ?? 0),
0,
)
const observedAtMs = time - probe.startedAt
const logicalRows = Number(tree?.dataset.totalRows ?? rows.length)
const ready =
logicalRows === expectedRows &&
fileRows.length > 0 &&
header.includes(basename) &&
viewers.length === 1 &&
diffLines > 0
const previous = probe.samples.at(-1)
const stable =
ready &&
previous?.ready === true &&
previous.logicalRows === logicalRows &&
previous.treeRows === rows.length &&
previous.fileRows === fileRows.length &&
previous.diffLines === diffLines &&
previous.header === header
probe.frameTimesMs.push(observedAtMs)
probe.samples.push({
observedAtMs,
logicalRows,
treeRows: rows.length,
fileRows: fileRows.length,
diffLines,
header,
ready,
})
if (probe.firstTreeRowMs === undefined && rows.length > 0) probe.firstTreeRowMs = observedAtMs
if (probe.logicalTreeReadyMs === undefined && logicalRows === expectedRows)
probe.logicalTreeReadyMs = observedAtMs
if (probe.firstDiffRenderMs === undefined && diffLines > 0) probe.firstDiffRenderMs = observedAtMs
readyStreak = !ready ? 0 : stable ? readyStreak + 1 : 1
if (readyStreak === stableFrames) probe.stableReadyMs = observedAtMs
if (probe.stableReadyMs === undefined) requestAnimationFrame(sample)
}
longTaskObserver?.observe({ type: "longtask", buffered: true })
document.addEventListener(
"click",
(event) => {
const toggle = event.target instanceof Element ? event.target.closest("button") : undefined
if (!toggle?.hasAttribute("data-review-pane-scaling-toggle")) return
probe.startedAt = performance.now()
performance.mark("opencode.review-pane-scaling.click")
requestAnimationFrame(sample)
},
{ capture: true, once: true },
)
;(window as Window & { __reviewPaneScalingProbe?: ReviewPaneScalingProbe }).__reviewPaneScalingProbe = probe
},
{ ...input, stableFrames: readyFrames },
)
}
@@ -0,0 +1,151 @@
import type { Page } from "@playwright/test"
import { expectSessionTitle } from "../../utils/waits"
import { mockOpenCodeServer } from "../../utils/mock-server"
import { benchmark, expect, withBenchmarkPage } from "../benchmark"
import { fixture } from "./session-timeline-stress.fixture"
import { installStressSessionTabs, stressSessionHref } from "./timeline-test-helpers"
import { measureSessionSwitch, waitForStableTimeline } from "./session-tab-switch-probe"
type ParentHydrationBenchmarkMode = "natural" | "candidate"
const mode = process.env.SESSION_PARENT_HYDRATION_BENCHMARK_MODE ?? "natural"
if (mode !== "natural" && mode !== "candidate") throw new Error(`Unknown parent hydration benchmark mode: ${mode}`)
const userID = "msg_parent_hydration_user"
const user = {
...fixture.messages[fixture.targetID][0]!,
info: { ...fixture.messages[fixture.targetID][0]!.info, id: userID, time: { created: 1700001000000 } },
parts: fixture.messages[fixture.targetID][0]!.parts.map((part, index) => ({
...part,
id: `prt_parent_hydration_user_${index}`,
messageID: userID,
})),
}
const assistantSeed = fixture.messages[fixture.targetID][3]!
const assistants = Array.from({ length: 14 }, (_, index) => {
const messageID = `msg_parent_hydration_${String(index).padStart(2, "0")}`
return {
...assistantSeed,
info: {
...assistantSeed.info,
id: messageID,
parentID: userID,
time: { created: 1700001001000 + index * 1_000, completed: 1700001001500 + index * 1_000 },
},
parts: assistantSeed.parts.map((part, partIndex) => ({
...part,
id: `prt_parent_hydration_${String(index).padStart(2, "0")}_${partIndex}`,
messageID,
})),
}
})
const messages = [user, ...assistants]
const target = fixture.sessions.find((session) => session.id === fixture.targetID)!
const lastID = userID
const lastPartID = assistants.at(-1)!.parts.at(-1)!.id
benchmark("hydrates an orphaned latest turn after a cold session click", async ({ browser, report }, testInfo) => {
benchmark.setTimeout(180_000)
const results = [] as Awaited<ReturnType<typeof trial>>[]
for (let run = 0; run < 5; run++) {
results.push(
await withBenchmarkPage(
browser,
`session-parent-hydration-${mode}-${run}`,
(page) => trial(page, mode),
testInfo,
),
)
}
const timing = results.map((result) => result.metrics.firstCorrectObservedMs!).sort((a, b) => a - b)
report(
{
results: results.map((result) => ({ ...result.metrics, historyGateCount: result.historyGateCount })),
summary: {
firstCorrectObservedMs: { min: timing[0], median: timing[2], max: timing.at(-1) },
blankSamples: results.map((result) => result.metrics.blankSamples),
requestCounts: {
list: results.map((result) => result.requestCounts.list),
parent: results.map((result) => result.requestCounts.parent),
},
historyGateCount: results.map((result) => result.historyGateCount),
},
},
{ mode },
)
})
async function trial(page: Page, mode: ParentHydrationBenchmarkMode) {
const requests: { type: "list" | "parent"; before?: string }[] = []
const history = mode === "candidate" ? Promise.withResolvers<void>() : undefined
let historyGates = 0
await mockOpenCodeServer(page, {
sessions: fixture.sessions.filter((session) => session.id === fixture.sourceID),
provider: fixture.provider,
directory: fixture.directory,
project: fixture.project,
messageDelay: 50,
onMessages: (request) => {
if (request.sessionID === fixture.targetID && request.phase === "start")
requests.push({ type: "list", before: request.before })
},
beforeMessagesResponse: (request) => {
if (mode !== "candidate" || request.sessionID !== fixture.targetID || !request.before) return Promise.resolve()
historyGates++
return history!.promise
},
onMessage: (request) => {
if (request.sessionID === fixture.targetID && request.messageID === userID) requests.push({ type: "parent" })
},
message: (sessionID, messageID) => {
if (sessionID !== fixture.targetID || messageID !== userID) return
return user
},
pageMessages: (sessionID, limit, before) => {
const items = sessionID === fixture.targetID ? messages : fixture.messages[fixture.sourceID]
const end = before ? items.findIndex((message) => message.info.id === before) : items.length
const start = Math.max(0, end - limit)
return { items: items.slice(start, end), cursor: start > 0 ? items[start]!.info.id : undefined }
},
})
await page.route(`**/session/${fixture.targetID}`, (route) =>
route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(target) }),
)
await installStressSessionTabs(page, { sessionIDs: [fixture.sourceID] })
await page.goto(stressSessionHref(fixture.sourceID))
await expectSessionTitle(page, fixture.expected.sourceTitle)
await waitForStableTimeline(page, fixture.expected.sourceMessageIDs.at(-1)!)
const href = stressSessionHref(fixture.targetID)
await page.evaluate(
({ href, title }) => {
const link = document.createElement("a")
link.id = "parent-hydration-target"
link.href = href
link.textContent = title
document.body.append(link)
},
{ href, title: target.title },
)
const metrics = await measureSessionSwitch(page, {
destinationIDs: messages.map((message) => message.info.id),
sourceIDs: fixture.messages[fixture.sourceID].map((message) => message.info.id),
lastID,
requiredPartID: lastPartID,
requireBottomAnchor: false,
href,
switch: async () => {
await page.locator("#parent-hydration-target").click()
await expectSessionTitle(page, target.title)
},
}).finally(() => history?.resolve())
expect(metrics.firstCorrectObservedMs).not.toBeNull()
const requestCounts = {
list: requests.filter((request) => request.type === "list").length,
parent: requests.filter((request) => request.type === "parent").length,
}
if (mode === "candidate") {
expect(requestCounts.parent).toBe(1)
expect(historyGates).toBe(1)
}
return { metrics, requestCounts, historyGateCount: historyGates }
}
@@ -2,7 +2,13 @@ import type { Page } from "@playwright/test"
import { expectSessionTitle } from "../../utils/waits"
import { benchmark, expect, withBenchmarkPage } from "../benchmark"
import { fixture } from "./session-timeline-stress.fixture"
import { installStressSessionTabs, mockStressTimeline, stressSessionHref } from "./timeline-test-helpers"
import {
createReviewDiffs,
installStressSessionTabs,
installTimelineSettings,
mockStressTimeline,
stressSessionHref,
} from "./timeline-test-helpers"
import { measureSessionSwitch, waitForStableTimeline } from "./session-tab-switch-probe"
type Result = Awaited<ReturnType<typeof measureSessionSwitch>>
@@ -20,8 +26,41 @@ benchmark("benchmarks cold and hot session tab switching", async ({ browser, rep
report({ results, summary: summarize(results) })
})
async function trial(page: Page, mode: "cold" | "hot") {
await mockStressTimeline(page)
benchmark(
"benchmarks v2 session tab switching with and without the review pane",
async ({ browser, report }, testInfo) => {
benchmark.setTimeout(360_000)
const runs = Number(process.env.SESSION_TAB_SWITCH_RUNS ?? 5)
const results = {
closed: { cold: [] as Result[], hot: [] as Result[] },
open: { cold: [] as Result[], hot: [] as Result[] },
}
for (const reviewPane of ["closed", "open"] as const) {
for (const mode of ["cold", "hot"] as const) {
for (let run = 0; run < runs; run++) {
results[reviewPane][mode].push(
await withBenchmarkPage(
browser,
`session-tab-switch-v2-${reviewPane}-${mode}-${run}`,
(page) => trial(page, mode, { newLayoutDesigns: true, reviewPane }),
testInfo,
),
)
}
}
}
report({ results, summary: summarizeReviewPane(results) }, { runs, reviewDiffs: createReviewDiffs().length })
},
)
async function trial(
page: Page,
mode: "cold" | "hot",
options?: { newLayoutDesigns?: boolean; reviewPane?: "closed" | "open" },
) {
const reviewDiffs = options?.newLayoutDesigns ? createReviewDiffs() : undefined
await mockStressTimeline(page, { vcsDiff: reviewDiffs })
if (options?.newLayoutDesigns) await installTimelineSettings(page)
await installStressSessionTabs(page)
if (mode === "hot") {
await page.goto(stressSessionHref(fixture.targetID))
@@ -33,6 +72,10 @@ async function trial(page: Page, mode: "cold" | "hot") {
await expectSessionTitle(page, fixture.expected.sourceTitle)
}
await waitForStableTimeline(page, fixture.expected.sourceMessageIDs.at(-1)!)
if (options?.reviewPane === "open") {
await openReviewPane(page)
await waitForStableTimeline(page, fixture.expected.sourceMessageIDs.at(-1)!)
}
const destinationIDs = fixture.messages[fixture.targetID].map((message) => message.info.id)
const sourceIDs = fixture.messages[fixture.sourceID].map((message) => message.info.id)
@@ -70,6 +113,15 @@ function summarize(results: Record<"cold" | "hot", Result[]>) {
)
}
function summarizeReviewPane(results: Record<"closed" | "open", Record<"cold" | "hot", Result[]>>) {
return Object.fromEntries(
Object.entries(results).map(([reviewPane, values]) => [
reviewPane,
summarize(values as Record<"cold" | "hot", Result[]>),
]),
)
}
async function switchSession(page: Page, sessionID: string, title: string) {
const href = stressSessionHref(sessionID)
const tab = page.locator(`[data-slot="titlebar-tabs"] a[href="${href}"]`).first()
@@ -77,3 +129,16 @@ async function switchSession(page: Page, sessionID: string, title: string) {
await tab.click()
await expectSessionTitle(page, title)
}
async function openReviewPane(page: Page) {
await page.getByRole("button", { name: "Toggle review" }).click()
const panel = page.locator("#review-panel")
await expect(panel).toBeVisible()
// Text-based readiness works across review implementations; the legacy list mounts
// diff viewers lazily while V2 mounts the active preview eagerly.
await page.waitForFunction(() => {
const panel = document.querySelector<HTMLElement>("#review-panel")
const text = panel?.textContent ?? ""
return text.includes("generated-000.ts") && text.includes("+3")
})
}
@@ -4,7 +4,15 @@ export type SessionSwitchSample = {
source: string[]
hasVisibleRows: boolean
last: boolean
requiredPartVisible?: boolean
bottomAnchorRequired?: boolean
bottomErrorPx?: number
review?: {
fileHost: boolean
fileHostReplaced: boolean
header: string
replacedLevels: string[]
}
}
export function classifySessionSwitch(samples: SessionSwitchSample[]) {
@@ -23,6 +31,10 @@ export function classifySessionSwitch(samples: SessionSwitchSample[]) {
(sample) => sample.hasVisibleRows && sample.destination.length === 0 && sample.source.length === 0,
).length,
sourceSamples: samples.filter((sample) => sample.source.length > 0).length,
reviewFileHostMissingSamples: samples.filter((sample) => sample.review && !sample.review.fileHost).length,
reviewFileHostReplacedSamples: samples.filter((sample) => sample.review?.fileHostReplaced).length,
reviewHeaders: [...new Set(samples.flatMap((sample) => (sample.review ? [sample.review.header] : [])))],
reviewReplacedLevels: [...new Set(samples.flatMap((sample) => sample.review?.replacedLevels ?? []))],
}
}
@@ -31,7 +43,8 @@ export function isCorrectDestination(sample: SessionSwitchSample) {
sample.destination.length > 0 &&
sample.source.length === 0 &&
sample.last &&
Math.abs(sample.bottomErrorPx ?? Infinity) <= 1
sample.requiredPartVisible !== false &&
(sample.bottomAnchorRequired === false || Math.abs(sample.bottomErrorPx ?? Infinity) <= 1)
)
}
@@ -8,19 +8,56 @@ type SessionSwitchProbe = {
async function installSessionSwitchProbe(
page: Page,
input: { destinationIDs: string[]; sourceIDs: string[]; lastID: string; href: string },
input: {
destinationIDs: string[]
sourceIDs: string[]
lastID: string
requiredPartID?: string
requireBottomAnchor?: boolean
href: string
},
) {
await page.evaluate(({ destinationIDs, sourceIDs, lastID, href }) => {
await page.evaluate(({ destinationIDs, sourceIDs, lastID, requiredPartID, requireBottomAnchor, href }) => {
const destination = new Set(destinationIDs)
const source = new Set(sourceIDs)
const samples: SessionSwitchSample[] = []
let started: number | undefined
let running = true
const reviewLevels: Record<string, string> = {
panel: "#review-panel",
tabs: '#review-panel [data-component="tabs"]',
body: '#review-panel [data-slot="session-review-v2-body"]',
review: '#review-panel [data-component="session-review-v2"]',
preview: '#review-panel [data-slot="session-review-v2-preview"]',
scroll: '#review-panel [data-slot="session-review-v2-diff-scroll"]',
file: '#review-panel [data-component="file"][data-mode="diff"]',
}
const initialReviewNodes: Record<string, Element | null> = {}
const sample = () => {
if (!running || started === undefined) return
setTimeout(() => {
if (!running || started === undefined) return
const observedAtMs = performance.now() - started
const reviewPanel = document.querySelector<HTMLElement>("#review-panel")
const reviewFile = reviewPanel?.querySelector('[data-component="file"][data-mode="diff"]')
const initialReviewFile = initialReviewNodes.file
const replacedLevels = Object.entries(reviewLevels).flatMap(([name, selector]) => {
const initial = initialReviewNodes[name]
if (!initial) return []
const current = document.querySelector(selector)
return current && current !== initial ? [name] : []
})
const review = reviewPanel
? {
fileHost: !!reviewFile,
fileHostReplaced: !!initialReviewFile && !!reviewFile && reviewFile !== initialReviewFile,
header:
reviewPanel
.querySelector<HTMLElement>('[data-slot="session-review-v2-file-header"]')
?.textContent?.trim() ?? "",
replacedLevels,
}
: undefined
const root = [...document.querySelectorAll<HTMLElement>(".scroll-view__viewport")].find((element) =>
element.querySelector("[data-timeline-row]"),
)
@@ -36,6 +73,13 @@ async function installSessionSwitchProbe(
const rect = element.getBoundingClientRect()
return rect.bottom > view.top && rect.top < view.bottom
})
const requiredPartVisible = requiredPartID
? [...root.querySelectorAll<HTMLElement>("[data-timeline-part-id]")].some((element) => {
if (element.dataset.timelinePartId !== requiredPartID) return false
const rect = element.getBoundingClientRect()
return rect.width > 0 && rect.height > 0 && rect.bottom > view.top && rect.top < view.bottom
})
: undefined
const spacer = root.querySelector<HTMLElement>('[data-timeline-row="bottom-spacer"]')?.getBoundingClientRect()
samples.push({
observedAtMs,
@@ -43,10 +87,22 @@ async function installSessionSwitchProbe(
source: visible.filter((id) => source.has(id)),
hasVisibleRows,
last: visible.includes(lastID),
requiredPartVisible,
bottomAnchorRequired: requireBottomAnchor !== false,
bottomErrorPx: spacer ? spacer.bottom - view.bottom : undefined,
review,
})
} else {
samples.push({ observedAtMs, destination: [], source: [], hasVisibleRows: false, last: false })
samples.push({
observedAtMs,
destination: [],
source: [],
hasVisibleRows: false,
last: false,
requiredPartVisible: requiredPartID ? false : undefined,
bottomAnchorRequired: requireBottomAnchor !== false,
review,
})
}
requestAnimationFrame(sample)
}, 0)
@@ -57,6 +113,9 @@ async function installSessionSwitchProbe(
const link = event.target instanceof Element ? event.target.closest("a") : undefined
if (link?.getAttribute("href") !== href) return
started = performance.now()
for (const [name, selector] of Object.entries(reviewLevels)) {
initialReviewNodes[name] = document.querySelector(selector)
}
requestAnimationFrame(sample)
},
{ capture: true, once: true },
@@ -83,7 +142,8 @@ async function waitForStableSessionSwitch(page: Page) {
sample.destination.length > 0 &&
sample.source.length === 0 &&
sample.last &&
Math.abs(sample.bottomErrorPx ?? Infinity) <= 1,
sample.requiredPartVisible !== false &&
(sample.bottomAnchorRequired === false || Math.abs(sample.bottomErrorPx ?? Infinity) <= 1),
)
)
})
@@ -101,13 +161,27 @@ async function collectSessionSwitchResult(page: Page) {
export async function measureSessionSwitch(
page: Page,
input: { destinationIDs: string[]; sourceIDs: string[]; lastID: string; href: string; switch: () => Promise<void> },
input: {
destinationIDs: string[]
sourceIDs: string[]
lastID: string
requiredPartID?: string
requireBottomAnchor?: boolean
href: string
switch: () => Promise<void>
},
) {
const { switch: run, ...probe } = input
await installSessionSwitchProbe(page, probe)
await run()
await waitForStableSessionSwitch(page)
return collectSessionSwitchResult(page)
try {
await run()
await waitForStableSessionSwitch(page)
return await collectSessionSwitchResult(page)
} finally {
await page.evaluate(() => {
;(window as Window & { __sessionSwitchProbe?: SessionSwitchProbe }).__sessionSwitchProbe?.stop()
})
}
}
export async function waitForStableTimeline(page: Page, lastID: string) {
@@ -93,36 +93,53 @@ const assistantMessage = {
parts: [editPart],
}
export async function setupTimelineBenchmark(page: Page, options: { historyTurns: number; eventBatch: number }) {
export async function setupTimelineBenchmark(
page: Page,
options: {
historyTurns: number
eventBatch: number
newLayoutDesigns?: boolean
vcsDiff?: unknown[]
turnDiffs?: unknown[]
},
) {
const events: EventPayload[] = []
let eventBatch = options.eventBatch
const currentUserMessage = options.turnDiffs
? { ...userMessage, info: { ...userMessage.info, summary: { diffs: options.turnDiffs } } }
: userMessage
await mockOpenCodeServer(page, {
directory,
project: project(),
provider: provider(),
sessions: [session()],
vcsDiff: options.vcsDiff,
pageMessages: () => ({
items: [
...Array.from({ length: options.historyTurns }, (_, index) => performanceTurn(index)).flat(),
userMessage,
currentUserMessage,
assistantMessage,
],
}),
events: () => events.splice(0, eventBatch),
eventRetry: 16,
})
await page.addInitScript(() => {
localStorage.setItem(
"settings.v3",
JSON.stringify({
general: {
editToolPartsExpanded: true,
shellToolPartsExpanded: true,
showReasoningSummaries: true,
},
}),
)
})
await page.addInitScript(
(input) => {
localStorage.setItem(
"settings.v3",
JSON.stringify({
general: {
newLayoutDesigns: input.newLayoutDesigns,
editToolPartsExpanded: true,
shellToolPartsExpanded: true,
showReasoningSummaries: true,
},
}),
)
},
{ newLayoutDesigns: options.newLayoutDesigns ?? false },
)
await page.setViewportSize({ width: 1366, height: 768 })
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
const text = page.locator(`[data-timeline-part-id="${textPartID}"]`).first()
@@ -1,3 +1,4 @@
import type { Page } from "@playwright/test"
import { benchmark, benchmarkDiagnostics, expect } from "../benchmark"
import {
buildInitialStreamEvent,
@@ -6,80 +7,300 @@ import {
textPartID,
} from "./session-timeline-benchmark.fixture"
import { startTimelineProfile } from "./session-timeline-profile"
import { createReviewDiffs } from "./timeline-test-helpers"
import {
collectTimelineStreamMetrics,
installTimelineStreamProbe,
startTimelineStreamProbe,
} from "./session-timeline-stream-probe"
type TimelineStreamOptions = {
newLayoutDesigns?: boolean
reviewDiffs?: boolean
reviewPane?: boolean
}
type ReviewPaneSample = {
observedAtMs: number
panelVisible: boolean
header: string
diffViewers: number
diffLines: number
codeBlocks: number
ready: boolean
}
type ReviewPaneProbe = {
samples: ReviewPaneSample[]
start: () => void
stop: () => void
}
const reviewReadyStreak = 3
benchmark.describe("performance: session timeline streaming", () => {
benchmark("streams assistant text without remounting or oscillating", async ({ page, report }) => {
benchmark.setTimeout(480_000)
const cpuThrottle = Number(process.env.TIMELINE_CPU_THROTTLE ?? 30)
const deltaCount = Number(process.env.TIMELINE_DELTA_COUNT ?? 160)
const historyTurns = Number(process.env.TIMELINE_HISTORY_TURNS ?? 320)
const eventBatch = Number(process.env.TIMELINE_EVENT_BATCH ?? 1)
const minimal = process.env.TIMELINE_MINIMAL === "1"
const profileCPU = process.env.TIMELINE_CPU_PROFILE === "1"
const profileVisual = !minimal && profileCPU && process.env.TIMELINE_VISUAL_PROFILE !== "0"
benchmark.setTimeout(Number(process.env.TIMELINE_COMPLETION_TIMEOUT_MS ?? 420_000) + 60_000)
const result = await runTimelineStreamBenchmark(page, {})
report(result.metrics, result.context)
})
benchmark("streams assistant text in v2 with review pane closed", async ({ page, report }) => {
benchmark.setTimeout(Number(process.env.TIMELINE_COMPLETION_TIMEOUT_MS ?? 420_000) + 60_000)
const result = await runTimelineStreamBenchmark(page, { newLayoutDesigns: true })
report(result.metrics, result.context)
})
benchmark("streams assistant text in v2 with review diffs and pane closed", async ({ page, report }) => {
benchmark.setTimeout(Number(process.env.TIMELINE_COMPLETION_TIMEOUT_MS ?? 420_000) + 60_000)
const result = await runTimelineStreamBenchmark(page, { newLayoutDesigns: true, reviewDiffs: true })
report(result.metrics, result.context)
})
benchmark("streams assistant text in v2 with review pane open", async ({ page, report }) => {
benchmark.setTimeout(Number(process.env.TIMELINE_COMPLETION_TIMEOUT_MS ?? 420_000) + 60_000)
const result = await runTimelineStreamBenchmark(page, { newLayoutDesigns: true, reviewPane: true })
report(result.metrics, result.context)
})
})
benchmark.describe("performance: review pane", () => {
benchmark("loads v2 review diffs and switches active files", async ({ page, report }) => {
benchmark.setTimeout(240_000)
const historyTurns = Number(process.env.REVIEW_PANE_HISTORY_TURNS ?? 72)
const diffs = createReviewDiffs()
const fixture = await setupTimelineBenchmark(page, {
historyTurns,
eventBatch,
eventBatch: 1,
newLayoutDesigns: true,
vcsDiff: diffs,
})
fixture.transport.enqueue(buildInitialStreamEvent(deltaCount))
const contentStart = performance.now()
fixture.transport.enqueue(buildInitialStreamEvent(1))
await expect(fixture.text).toBeVisible()
await expect(fixture.text).toContainText("Implementation plan")
const initialContentObservedMs = performance.now() - contentStart
await fixture.scrollToBottom()
await fixture.waitForStableGeometry()
const profile = await startTimelineProfile(page, { cpuThrottle, profileCPU })
await installTimelineStreamProbe(page, { textPartID, finalIndex: deltaCount, profileVisual, minimal })
const deltas = buildStreamDeltaEvents(deltaCount)
await startTimelineStreamProbe(page)
fixture.transport.enqueue(deltas)
await page.waitForFunction(
(finalIndex) =>
(
window as Window & {
__timelineStreamBenchmark?: { applied: { index: number }[] }
}
).__timelineStreamBenchmark?.applied.some((value) => value.index === finalIndex),
deltaCount,
{ timeout: 420_000 },
)
await expect(fixture.text).toContainText("benchmark-complete")
await expect(fixture.text).toContainText("Streaming")
await fixture.waitForStableGeometry()
const metrics = await collectTimelineStreamMetrics(page, {
textPartID,
finalIndex: deltaCount,
navigations: benchmarkDiagnostics(page).navigations,
})
const delivered = deltas.length - fixture.transport.pendingCount()
await profile.stop()
const open = await measureReviewPaneLoad(page, diffs[0]!.file)
const switches = []
for (const diff of diffs.slice(1, 4)) switches.push(await measureReviewNextFile(page, diff.file))
report(
{
endToEndInitialContentObservedMs: initialContentObservedMs,
...metrics,
deliveredDeltas: delivered,
pendingDeltas: fixture.transport.pendingCount(),
open,
switches,
},
{
cpuThrottle,
profileCPU,
profileVisual,
minimal,
queuedDeltas: deltas.length,
historyTurns,
eventBatch,
reviewDiffs: diffs.length,
},
)
await profile.reset()
})
})
async function runTimelineStreamBenchmark(page: Page, options: TimelineStreamOptions) {
const completionTimeoutMs = Number(process.env.TIMELINE_COMPLETION_TIMEOUT_MS ?? 420_000)
const cpuThrottle = Number(process.env.TIMELINE_CPU_THROTTLE ?? 30)
const deltaCount = Number(process.env.TIMELINE_DELTA_COUNT ?? 160)
const historyTurns = Number(process.env.TIMELINE_HISTORY_TURNS ?? 320)
const eventBatch = Number(process.env.TIMELINE_EVENT_BATCH ?? 1)
const minimal = process.env.TIMELINE_MINIMAL === "1"
const profileCPU = process.env.TIMELINE_CPU_PROFILE === "1"
const profileVisual = !minimal && profileCPU && process.env.TIMELINE_VISUAL_PROFILE !== "0"
const diffs = options.reviewDiffs || options.reviewPane ? createReviewDiffs() : undefined
const fixture = await setupTimelineBenchmark(page, {
historyTurns,
eventBatch,
newLayoutDesigns: options.newLayoutDesigns,
// Turn diffs exercise timeline data cost; the pane-open scenario serves the same
// diffs through the default git mode so it works across review implementations.
turnDiffs: options.reviewDiffs ? diffs : undefined,
vcsDiff: options.reviewPane ? diffs : undefined,
})
fixture.transport.enqueue(buildInitialStreamEvent(deltaCount))
const contentStart = performance.now()
await expect(fixture.text).toBeVisible()
await expect(fixture.text).toContainText("Implementation plan")
const initialContentObservedMs = performance.now() - contentStart
await fixture.scrollToBottom()
await fixture.waitForStableGeometry()
const reviewPane = options.reviewPane && diffs ? await measureReviewPaneLoad(page, diffs[0]!.file) : undefined
if (reviewPane) await fixture.waitForStableGeometry()
const profile = await startTimelineProfile(page, { cpuThrottle, profileCPU })
await installTimelineStreamProbe(page, { textPartID, finalIndex: deltaCount, profileVisual, minimal })
const deltas = buildStreamDeltaEvents(deltaCount)
await startTimelineStreamProbe(page)
fixture.transport.enqueue(deltas)
await page.waitForFunction(
(finalIndex) =>
(
window as Window & {
__timelineStreamBenchmark?: { applied: { index: number }[] }
}
).__timelineStreamBenchmark?.applied.some((value) => value.index === finalIndex),
deltaCount,
{ timeout: completionTimeoutMs },
)
await expect(fixture.text).toContainText("benchmark-complete")
await expect(fixture.text).toContainText("Streaming")
await fixture.waitForStableGeometry()
const metrics = await collectTimelineStreamMetrics(page, {
textPartID,
finalIndex: deltaCount,
navigations: benchmarkDiagnostics(page).navigations,
})
const delivered = deltas.length - fixture.transport.pendingCount()
await profile.stop()
const result = {
metrics: {
endToEndInitialContentObservedMs: initialContentObservedMs,
...metrics,
deliveredDeltas: delivered,
pendingDeltas: fixture.transport.pendingCount(),
reviewPane: reviewPane ?? null,
},
context: {
cpuThrottle,
profileCPU,
profileVisual,
minimal,
queuedDeltas: deltas.length,
historyTurns,
eventBatch,
newLayoutDesigns: options.newLayoutDesigns === true,
reviewPane: options.reviewPane === true ? "open" : "closed",
reviewDiffs: diffs?.length ?? 0,
},
}
await profile.reset()
return result
}
async function measureReviewPaneLoad(page: Page, file: string) {
// Default git mode reads the mocked /vcs/diff data, so opening the pane is enough
// and the flow works across review pane implementations.
await installReviewPaneProbe(page, { file })
await startReviewPaneProbe(page)
await page.getByRole("button", { name: "Toggle review" }).click()
await expect(page.locator("#review-panel")).toBeVisible()
return collectReviewPaneProbe(page)
}
async function measureReviewNextFile(page: Page, file: string) {
await installReviewPaneProbe(page, { file })
await startReviewPaneProbe(page)
await page.getByRole("button", { name: "Next file" }).click()
return collectReviewPaneProbe(page)
}
async function installReviewPaneProbe(page: Page, input: { file: string }) {
await page.evaluate((input) => {
const samples: ReviewPaneSample[] = []
const basename = input.file.split(/[\\/]/).at(-1) ?? input.file
let started: number | undefined
let running = true
const paneState = () => {
const panel = document.querySelector<HTMLElement>("#review-panel")
const review = panel?.querySelector<HTMLElement>('[data-component="session-review-v2"]')
const rect = (review ?? panel)?.getBoundingClientRect()
const text = panel?.textContent ?? ""
const previewHeader = panel?.querySelector<HTMLElement>(
'[data-slot="session-review-v2-file-header"]',
)?.textContent
const header = previewHeader ?? text
const viewers = panel ? [...panel.querySelectorAll<HTMLElement>('[data-component="file"][data-mode="diff"]')] : []
const codeBlocks = panel?.querySelectorAll("code").length ?? 0
const diffLines = viewers.reduce(
(sum, viewer) =>
sum +
(viewer.shadowRoot?.querySelectorAll("[data-line]").length ?? viewer.querySelectorAll("[data-line]").length),
0,
)
const panelVisible =
!!panel && panel.getAttribute("aria-hidden") !== "true" && !!rect && rect.width > 0 && rect.height > 0
return {
panelVisible,
header: header.slice(0, 500),
diffViewers: viewers.length,
diffLines,
codeBlocks,
ready:
panelVisible &&
header.includes(basename) &&
(viewers.length > 0 || text.includes("+3") || diffLines > 0 || codeBlocks > 0),
}
}
const sample = () => {
if (!running || started === undefined) return
requestAnimationFrame(() => {
setTimeout(() => {
if (!running || started === undefined) return
samples.push({ observedAtMs: performance.now() - started, ...paneState() })
if (performance.now() - started < 10_000) sample()
}, 0)
})
}
;(window as Window & { __reviewPaneProbe?: ReviewPaneProbe }).__reviewPaneProbe = {
samples,
start: () => {
started = performance.now()
performance.mark("opencode.review-pane.click")
sample()
},
stop: () => {
running = false
},
}
}, input)
}
async function startReviewPaneProbe(page: Page) {
await page.evaluate(() => {
;(window as Window & { __reviewPaneProbe?: ReviewPaneProbe }).__reviewPaneProbe!.start()
})
}
async function collectReviewPaneProbe(page: Page) {
await page.waitForFunction((streak) => {
const samples = (window as Window & { __reviewPaneProbe?: ReviewPaneProbe }).__reviewPaneProbe?.samples
if (!samples) return false
return samples.some((_, index) => {
const stable = samples.slice(index, index + streak)
return stable.length === streak && stable.every((sample) => sample.ready)
})
}, reviewReadyStreak)
const samples = await page.evaluate(() => {
const probe = (window as Window & { __reviewPaneProbe?: ReviewPaneProbe }).__reviewPaneProbe!
probe.stop()
return probe.samples
})
return { summary: summarizeReviewPaneSamples(samples), samples }
}
function summarizeReviewPaneSamples(samples: ReviewPaneSample[]) {
const firstReady = samples.find((sample) => sample.ready)
const stableIndex = samples.findIndex((_, index) => {
const stable = samples.slice(index, index + reviewReadyStreak)
return stable.length === reviewReadyStreak && stable.every((sample) => sample.ready)
})
return {
samples: samples.length,
firstReadyObservedMs: firstReady?.observedAtMs ?? null,
stableReadyObservedMs: stableIndex === -1 ? null : samples[stableIndex + reviewReadyStreak - 1]!.observedAtMs,
notReadySamples: samples.filter((sample) => !sample.ready).length,
maxDiffViewers: Math.max(0, ...samples.map((sample) => sample.diffViewers)),
maxDiffLines: Math.max(0, ...samples.map((sample) => sample.diffLines)),
maxCodeBlocks: Math.max(0, ...samples.map((sample) => sample.codeBlocks)),
}
}
@@ -21,7 +21,10 @@ export async function installTimelineSettings(page: Page) {
export function mockStressTimeline(
page: Page,
input?: { onMessages?: (input: { sessionID: string; before?: string; phase: "start" | "end" }) => void },
input?: {
onMessages?: (input: { sessionID: string; before?: string; phase: "start" | "end" }) => void
vcsDiff?: unknown[]
},
) {
return mockOpenCodeServer(page, {
sessions: fixture.sessions,
@@ -30,6 +33,7 @@ export function mockStressTimeline(
project: fixture.project,
pageMessages,
onMessages: input?.onMessages,
vcsDiff: input?.vcsDiff,
})
}
@@ -45,7 +49,7 @@ export async function installStressSessionTabs(page: Page, input?: { draftID?: s
}),
)
localStorage.setItem(
"opencode.global.dat:tabs",
"opencode.window.browser.dat:tabs",
JSON.stringify([
...sessionIDs.map((sessionId) => ({
type: "session",
@@ -78,3 +82,53 @@ export function stressDraftHref(draftID: string) {
function stressServer() {
return `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
}
export function createReviewDiffs() {
return Array.from({ length: Number(process.env.REVIEW_PANE_DIFF_COUNT ?? 72) }, (_, index) => {
const lines = index % 3 === 0 ? 300 : index % 3 === 1 ? 120 : 38
const file = `src/review/generated-${String(index).padStart(3, "0")}.ts`
const before = reviewSource(index, lines)
const after = before
.replace(`value_${index}_4`, `updated_${index}_4`)
.replace(
`value_${index}_${Math.max(8, Math.floor(lines / 2))}`,
`updated_${index}_${Math.max(8, Math.floor(lines / 2))}`,
)
.replace(`value_${index}_${lines - 4}`, `updated_${index}_${lines - 4}`)
return {
file,
patch: reviewPatch(file, before, after),
additions: 3,
deletions: 3,
status: "modified" as const,
}
})
}
function reviewSource(seed: number, lines: number) {
return Array.from(
{ length: lines },
(_, index) => `export const value_${seed}_${index} = "${reviewWords(seed + index, index % 5 === 0 ? 180 : 42)}"`,
).join("\n")
}
function reviewPatch(file: string, before: string, after: string) {
const beforeLines = before.split("\n")
const afterLines = after.split("\n")
return [
`diff --git a/${file} b/${file}`,
`--- a/${file}`,
`+++ b/${file}`,
`@@ -1,${beforeLines.length} +1,${afterLines.length} @@`,
...beforeLines.flatMap((line, index) => {
const next = afterLines[index]!
if (line === next) return [` ${line}`]
return [`-${line}`, `+${next}`]
}),
].join("\n")
}
function reviewWords(seed: number, length: number) {
const words = ["alpha", "bravo", "charlie", "delta", "echo", "foxtrot", "golf", "hotel", "india", "juliet"]
return Array.from({ length: Math.ceil(length / 7) }, (_, index) => words[(seed + index * 3) % words.length]).join(" ")
}
@@ -0,0 +1,46 @@
import { expect, test } from "bun:test"
import type { Page, Route } from "@playwright/test"
import { mockOpenCodeServer } from "../../utils/mock-server"
test("applies message latency after a list response gate is released", async () => {
const events: string[] = []
const gate = Promise.withResolvers<void>()
let handler: ((route: Route) => Promise<void>) | undefined
const page = {
route: (_url: string, callback: (route: Route) => Promise<void>) => {
handler = callback
return Promise.resolve()
},
} as unknown as Page
await mockOpenCodeServer(page, {
provider: {},
directory: "C:/OpenCode",
project: {},
sessions: [{ id: "session" }],
messageDelay: 25,
beforeMessagesResponse: () => {
events.push("before")
return gate.promise
},
onMessages: (request) => events.push(request.phase),
pageMessages: () => {
events.push("page")
return { items: [] }
},
})
const response = handler!({
request: () => ({ url: () => "http://127.0.0.1:4096/session/session/message" }),
fulfill: () => {
events.push("fulfill")
return Promise.resolve()
},
} as unknown as Route)
expect(events).toEqual(["start", "before"])
const released = performance.now()
gate.resolve()
await response
expect(performance.now() - released).toBeGreaterThanOrEqual(20)
expect(events).toEqual(["start", "before", "page", "end", "fulfill"])
})
@@ -52,3 +52,35 @@ test("reports missing correctness without throwing", () => {
expect(result.firstCorrectObservedMs).toBeNull()
expect(result.stableObservedMs).toBeNull()
})
test("requires an explicitly tracked part to be visible", () => {
const result = classifySessionSwitch([
{
observedAtMs: 16,
destination: ["destination"],
source: [],
hasVisibleRows: true,
last: true,
requiredPartVisible: false,
bottomErrorPx: 0,
},
])
expect(result.firstCorrectObservedMs).toBeNull()
})
test("can measure content correctness without requiring a bottom anchor", () => {
const result = classifySessionSwitch([
{
observedAtMs: 16,
destination: ["destination"],
source: [],
hasVisibleRows: true,
last: true,
requiredPartVisible: true,
bottomAnchorRequired: false,
},
])
expect(result.firstCorrectObservedMs).toBe(16)
})
@@ -0,0 +1,55 @@
import { expect, test } from "bun:test"
import type { Page } from "@playwright/test"
import { measureSessionSwitch } from "../timeline/session-tab-switch-probe"
function testPage(waitFailure?: Error) {
const stops: unknown[] = []
const page = {
evaluate: async (_callback: unknown, input?: unknown) => {
if (input) return
stops.push(undefined)
},
waitForFunction: async () => {
if (waitFailure) throw waitFailure
},
} as unknown as Page
return { page, stops }
}
function input(run: () => Promise<void>) {
return {
destinationIDs: ["destination"],
sourceIDs: ["source"],
lastID: "destination",
href: "/session/destination",
switch: run,
}
}
test("stops sampling when the session switch fails", async () => {
const failure = new Error("switch failed")
const context = testPage()
await expect(
measureSessionSwitch(
context.page,
input(async () => Promise.reject(failure)),
),
).rejects.toBe(failure)
expect(context.stops).toHaveLength(1)
})
test("stops sampling when the stable wait fails", async () => {
const failure = new Error("stable wait failed")
const context = testPage(failure)
await expect(
measureSessionSwitch(
context.page,
input(async () => {}),
),
).rejects.toBe(failure)
expect(context.stops).toHaveLength(1)
})
@@ -0,0 +1,392 @@
import { expect, test } from "bun:test"
import {
analyzeVisualStability,
analyzeVisualStabilityByMarker,
type VisualStabilityTrace,
} from "../../utils/visual-stability"
import { analyzeVisualObservations } from "../../utils/visual-stability/analyzer"
import { legacyVisualPlan, visualPlan, type VisualInvariant } from "../../utils/visual-stability/invariant"
import { defineVisualRegions, mapVisualRegions } from "../../utils/visual-stability/regions"
function trace(samples: VisualStabilityTrace["samples"]): VisualStabilityTrace {
return { markers: [], samples }
}
test("accepts continuous visible motion", () => {
expect(
analyzeVisualStability(
trace([
frame(0, region({ width: 80, bottom: 40 }), region({ top: 40, bottom: 60 })),
frame(16, region({ width: 75, bottom: 45 }), region({ top: 45, bottom: 65 })),
frame(32, region({ width: 70, bottom: 50 }), region({ top: 50, bottom: 70 })),
]),
{ flow: ["changing", "following"] },
),
).toEqual([])
})
test("reports repeated geometry reversals", () => {
const issues = analyzeVisualStability(
trace([
frame(0, region({ width: 80 })),
frame(16, region({ width: 60 })),
frame(32, region({ width: 78 })),
frame(48, region({ width: 62 })),
]),
)
expect(issues.some((issue) => issue.includes("changing width reversed 2 times"))).toBe(true)
})
test("reports visible blanking, label reversal, and overlap", () => {
const issues = analyzeVisualStability(
trace([
frame(0, region({ label: "Exploring", opacity: 1, bottom: 40 }), region({ top: 40, bottom: 60 })),
frame(16, region({ label: "Explored", opacity: 0.2, bottom: 50 }), region({ top: 49, bottom: 69 })),
frame(32, region({ label: "Exploring", opacity: 1, bottom: 50 }), region({ top: 50, bottom: 70 })),
]),
{ flow: ["changing", "following"] },
)
expect(issues.some((issue) => issue.includes("opacity fell to 0.2"))).toBe(true)
expect(issues.some((issue) => issue.includes("label reverted"))).toBe(true)
expect(issues.some((issue) => issue.includes("overlapped following by 1px"))).toBe(true)
})
test("reports duplicate regions and unexpected remounts", () => {
const issues = analyzeVisualStability(
trace([frame(0, region({ node: 1 })), frame(16, region({ node: 2, count: 2 })), frame(32, region({ node: 2 }))]),
{ stable: ["changing"], unique: ["changing"] },
)
expect(issues.some((issue) => issue.includes("changing appeared 2 times"))).toBe(true)
expect(issues.some((issue) => issue.includes("changing remounted"))).toBe(true)
})
test("reports bottom anchor loss but permits movement while scrolled away", () => {
const anchored = analyzeVisualStability(
trace([
{ at: 0, regions: { changing: region() }, viewport: viewport(0) },
{ at: 16, regions: { changing: region() }, viewport: viewport(24) },
]),
{ preserveBottomAnchor: true },
)
const away = analyzeVisualStability(
trace([
{ at: 0, regions: { changing: region() }, viewport: viewport(80) },
{ at: 16, regions: { changing: region() }, viewport: viewport(104) },
]),
{ preserveBottomAnchor: true },
)
expect(anchored.some((issue) => issue.includes("bottom anchor moved to 24px"))).toBe(true)
expect(away).toEqual([])
})
test("reports up down up movement while preserving a bottom anchor", () => {
const issues = analyzeVisualStability(
trace([
{ at: 0, regions: { changing: region({ top: 200, bottom: 240 }) }, viewport: viewport(0) },
{ at: 16, regions: { changing: region({ top: 180, bottom: 220 }) }, viewport: viewport(0) },
{ at: 32, regions: { changing: region({ top: 196, bottom: 236 }) }, viewport: viewport(0) },
{ at: 48, regions: { changing: region({ top: 176, bottom: 216 }) }, viewport: viewport(0) },
]),
{ preserveBottomAnchor: true, maxPositionReversals: 0 },
)
expect(issues.some((issue) => issue.includes("changing top reversed 2 times"))).toBe(true)
expect(issues.some((issue) => issue.includes("changing bottom reversed 2 times"))).toBe(true)
})
test("accepts monotonic upward movement while preserving a bottom anchor", () => {
expect(
analyzeVisualStability(
trace([
{ at: 0, regions: { changing: region({ top: 200, bottom: 240 }) }, viewport: viewport(0) },
{ at: 16, regions: { changing: region({ top: 190, bottom: 230 }) }, viewport: viewport(0) },
{ at: 32, regions: { changing: region({ top: 180, bottom: 220 }) }, viewport: viewport(0) },
]),
{ preserveBottomAnchor: true, maxPositionReversals: 0 },
),
).toEqual([])
})
test("ignores overlap entirely outside the clipped timeline viewport", () => {
expect(
analyzeVisualStability(
trace([
{
at: 0,
regions: {
changing: region({ top: -200, bottom: -100 }),
following: region({ top: -150, bottom: -50 }),
},
viewport: viewport(0),
},
]),
{ flow: ["changing", "following"] },
),
).toEqual([])
})
test("reports visible anchor movement while allowing virtual scrollbar movement", () => {
const issues = analyzeVisualStability(
trace([
{ at: 0, regions: { anchor: region({ top: 100, bottom: 120 }) }, viewport: { ...viewport(100), scrollTop: 40 } },
{ at: 16, regions: { anchor: region({ top: 100, bottom: 120 }) }, viewport: { ...viewport(120), scrollTop: 60 } },
]),
{ fixed: ["anchor"] },
)
expect(issues).toEqual([])
const moved = analyzeVisualStability(
trace([
{ at: 0, regions: { anchor: region({ top: 100, bottom: 120 }) }, viewport: viewport(100) },
{ at: 16, regions: { anchor: region({ top: 112, bottom: 132 }) }, viewport: viewport(100) },
]),
{ fixed: ["anchor"] },
)
expect(moved.some((issue) => issue.includes("anchor moved 12px in the viewport"))).toBe(true)
})
test("analyzes each marked event independently", () => {
const input: VisualStabilityTrace = {
markers: [
{ at: 10, label: "grow" },
{ at: 40, label: "shrink" },
],
samples: [
frame(0, region({ top: 100 })),
frame(16, region({ top: 90 })),
frame(32, region({ top: 80 })),
frame(48, region({ top: 90 })),
frame(64, region({ top: 100 })),
],
}
expect(analyzeVisualStability(input, { maxPositionReversals: 0 })).toContain("changing top reversed 1 times")
expect(
analyzeVisualStabilityByMarker(input, {
maxPositionReversals: 0,
motion: ["changing"],
aggregateMotion: false,
}),
).toEqual([])
})
test("reports cross-event motion reversals by default", () => {
const input: VisualStabilityTrace = {
markers: [
{ at: 10, label: "up" },
{ at: 40, label: "down" },
],
samples: [
frame(0, region({ top: 100 })),
frame(16, region({ top: 90 })),
frame(32, region({ top: 80 })),
frame(48, region({ top: 90 })),
frame(64, region({ top: 100 })),
],
}
expect(analyzeVisualStabilityByMarker(input, { maxPositionReversals: 0 })).toContain("changing top reversed 1 times")
})
test("reports regions rendered in the wrong flow order", () => {
const issues = analyzeVisualStability(
trace([frame(0, region({ top: 100, bottom: 120 }), region({ top: 60, bottom: 80 }))]),
{ flow: ["changing", "following"] },
)
expect(issues.some((issue) => issue.includes("changing rendered after following"))).toBe(true)
})
test("uses painted bounds instead of clipped layout overflow", () => {
expect(
analyzeVisualStability(
trace([
frame(
0,
region({ top: 100, bottom: 140, height: 40, layoutTop: 100, layoutBottom: 300 }),
region({ top: 140, bottom: 180 }),
),
]),
{ flow: ["changing", "following"] },
),
).toEqual([])
})
test("does not report disappearance when a present row moves outside the viewport", () => {
expect(
analyzeVisualStability(
trace([
frame(0, region({ visible: true })),
frame(16, region({ visible: false, inViewport: false, top: -100, bottom: -80 })),
frame(32, region({ visible: true })),
]),
),
).toEqual([])
})
test("reports an in-viewport transparent frame between visible frames", () => {
const issues = analyzeVisualStability(
trace([
frame(0, region()),
frame(16, region({ visible: false, opacity: 0, inViewport: true })),
frame(32, region()),
]),
)
expect(issues.some((issue) => issue.includes("blanked between visible frames"))).toBe(true)
})
test("reports an in-viewport display-none frame between visible frames", () => {
const issues = analyzeVisualStability(
trace([
frame(0, region()),
frame(16, region({ visible: false, width: 0, height: 0, inViewport: true, cssHidden: true })),
frame(32, region()),
]),
)
expect(issues.some((issue) => issue.includes("blanked between visible frames"))).toBe(true)
})
test("can limit motion analysis to unaffected regions", () => {
expect(
analyzeVisualStability(
trace([
frame(0, region({ height: 20 }), region()),
frame(16, region({ height: 40 }), region()),
frame(32, region({ height: 30 }), region()),
]),
{ motion: ["following"] },
),
).toEqual([])
})
test("reports a blank frame across replacement surfaces", () => {
const issues = analyzeVisualStability(
{
markers: [],
samples: [
{ at: 0, regions: { thinking: region(), error: region({ present: false, visible: false }) } },
{
at: 16,
regions: {
thinking: region({ present: false, visible: false }),
error: region({ present: false, visible: false }),
},
},
{ at: 32, regions: { thinking: region({ present: false, visible: false }), error: region() } },
],
},
{ continuousAny: [["thinking", "error"]] },
)
expect(issues.some((issue) => issue.includes("thinking | error blanked"))).toBe(true)
})
test("reports failure to acquire the bottom anchor", () => {
const issues = analyzeVisualStability(
trace([
{ at: 0, regions: { changing: region() }, viewport: viewport(600) },
{ at: 16, regions: { changing: region() }, viewport: viewport(120) },
]),
{ acquireBottomAnchor: true },
)
expect(issues.some((issue) => issue.includes("did not acquire bottom anchor"))).toBe(true)
})
test("reports a required region that never renders", () => {
const issues = analyzeVisualStability(
trace([
{
at: 0,
regions: { changing: region({ present: false, visible: false }) },
},
]),
{ required: ["changing"] },
)
expect(issues).toContain("changing never rendered")
})
test("preserves typed region names while mapping definitions", () => {
const regions = defineVisualRegions({
changing: { selector: "[data-changing]" },
following: { selector: "[data-following]", closest: "[data-row]" },
})
const selectors = mapVisualRegions(regions, (region) => region.selector)
expect(selectors).toEqual({ changing: "[data-changing]", following: "[data-following]" })
const name: keyof typeof selectors = "changing"
expect(name).toBe("changing")
})
test("evaluates the typed invariant algebra over explicit observations", () => {
const regions = defineVisualRegions({
changing: { selector: "[data-changing]" },
following: { selector: "[data-following]" },
})
const invariants = [
{ type: "required", regions: ["changing"] },
{ type: "flow", regions: ["changing", "following"] },
] satisfies VisualInvariant<keyof typeof regions>[]
// @ts-expect-error Plans reject names that are not in the region definition.
const invalid = { type: "required", regions: ["missing"] } satisfies VisualInvariant<keyof typeof regions>
const plan = visualPlan(regions, invariants, { perMarker: true })
expect(invalid.regions).toEqual(["missing"])
expect(plan.perMarker).toBe(true)
expect(analyzeVisualObservations([frame(0, region({ bottom: 50 }), region({ top: 49, bottom: 69 }))], plan)).toEqual([
"changing overlapped following by 1px at 0ms",
])
})
test("legacy plan adapter preserves analyzer messages and order", () => {
const input = trace([
frame(0, region({ label: "Exploring", opacity: 1, bottom: 40 }), region({ top: 40, bottom: 60 })),
frame(16, region({ label: "Explored", opacity: 0.2, bottom: 50 }), region({ top: 49, bottom: 69 })),
frame(32, region({ label: "Exploring", opacity: 1, bottom: 50 }), region({ top: 50, bottom: 70 })),
])
const options = { flow: ["changing", "following"], stable: ["changing"] }
expect(analyzeVisualObservations(input.samples, legacyVisualPlan(options))).toEqual(
analyzeVisualStability(input, options),
)
})
function frame(
at: number,
changing: VisualStabilityTrace["samples"][number]["regions"][string],
following?: VisualStabilityTrace["samples"][number]["regions"][string],
) {
return { at, regions: { changing, ...(following ? { following } : {}) } }
}
function region(input: Partial<VisualStabilityTrace["samples"][number]["regions"][string]> = {}) {
return {
present: true,
visible: true,
inViewport: true,
top: 0,
bottom: 20,
width: 100,
height: 20,
opacity: 1,
count: 1,
node: 1,
label: "",
text: "",
layoutTop: input.top ?? 0,
layoutBottom: input.bottom ?? 20,
...input,
}
}
function viewport(distanceFromBottom: number) {
return { top: 0, bottom: 400, scrollTop: 100, scrollHeight: 500, clientHeight: 400, distanceFromBottom }
}
@@ -14,7 +14,7 @@ test("closing the active server's last tab opens the remaining server tab", asyn
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] }))
localStorage.setItem(
"opencode.global.dat:tabs",
"opencode.window.browser.dat:tabs",
JSON.stringify([
{ type: "session", server: "http://127.0.0.1:4096", sessionId: sessionA },
{ type: "session", server: serverB, sessionId: sessionB },
@@ -52,7 +52,7 @@ test("legacy session routes preserve an existing tab's server", async ({ page })
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] }))
localStorage.setItem(
"opencode.global.dat:tabs",
"opencode.window.browser.dat:tabs",
JSON.stringify([{ type: "session", server: serverB, sessionId: sessionB }]),
)
},
@@ -0,0 +1,150 @@
import { base64Encode } from "@opencode-ai/core/util/encode"
import { expect, test, type Page } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectSessionTitle } from "../utils/waits"
const directory = "C:/OpenCode/FileBrowserSidebar"
const projectID = "proj_file_browser_sidebar"
const sessionID = "ses_file_browser_sidebar"
const title = "File browser sidebar"
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
const files = Array.from({ length: 80 }, (_, index) => `file-${String(index).padStart(2, "0")}.ts`)
// Marks the file-browser sidebar DOM node so a remount (fresh node) is detectable.
const PROBE = "original"
test.use({ viewport: { width: 1440, height: 900 } })
// The file-browser sidebar must stay mounted across preview/pinned file-tab
// switches. Remounting resets scroll and filter state.
test("keeps the file-browser sidebar mounted when switching file tabs", async ({ page }) => {
await setup(page)
await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
await expectSessionTitle(page, title)
const panel = page.locator("#review-panel")
await panel.getByRole("button", { name: "Open file" }).click()
await expect(panel.getByRole("tab", { name: "Open file" })).toHaveAttribute("data-selected", "")
const sidebar = panel.locator('[data-component="session-review-v2-sidebar-root"]')
await expect(sidebar).toBeVisible()
await expect(panel.getByRole("button", { name: "file-00.ts" })).toBeVisible()
await panel.getByRole("button", { name: "file-00.ts" }).click()
await expect(panel.getByRole("tab", { name: "file-00.ts" })).toHaveAttribute("data-selected", "")
await expect(panel.getByText("contents:file-00.ts", { exact: true })).toBeVisible()
const viewport = panel.locator('[data-slot="session-review-v2-sidebar-tree"] .scroll-view__viewport')
await viewport.hover()
await page.mouse.wheel(0, 100_000)
await expect
.poll(() => viewport.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop))
.toBeLessThanOrEqual(1)
const scrolled = await viewport.evaluate((element) => element.scrollTop)
expect(scrolled).toBeGreaterThan(0)
await writeProbe(page)
await panel.getByRole("button", { name: "file-79.ts" }).click()
await expect(panel.getByRole("tab", { name: "file-79.ts" })).toHaveAttribute("data-selected", "")
await expect(panel.getByText("contents:file-79.ts", { exact: true })).toBeVisible()
expect(await readProbe(page)).toBe(PROBE)
await expect.poll(() => viewport.evaluate((element) => element.scrollTop)).toBe(scrolled)
await panel.getByRole("button", { name: "file-78.ts" }).dblclick()
await expect(panel.getByRole("tab", { name: "file-78.ts" })).toHaveAttribute("data-selected", "")
await panel.getByRole("button", { name: "file-79.ts" }).click()
await expect(panel.getByRole("tab", { name: "file-79.ts" })).toHaveAttribute("data-selected", "")
await panel.getByRole("tab", { name: "file-78.ts" }).click()
await expect(panel.getByRole("tab", { name: "file-78.ts" })).toHaveAttribute("data-selected", "")
expect(await readProbe(page)).toBe(PROBE)
await expect.poll(() => viewport.evaluate((element) => element.scrollTop)).toBe(scrolled)
})
type Probed = HTMLElement & { __e2eProbe?: string }
async function writeProbe(page: Page) {
await page.locator('#review-panel [data-component="session-review-v2-sidebar-root"]').evaluate((el, probe) => {
;(el as Probed).__e2eProbe = probe
}, PROBE)
}
async function readProbe(page: Page) {
return page
.locator('#review-panel [data-component="session-review-v2-sidebar-root"]')
.evaluate((el) => (el as Probed).__e2eProbe)
}
async function setup(page: Page) {
await mockOpenCodeServer(page, {
directory,
project: {
id: projectID,
worktree: directory,
vcs: "git",
name: "file-browser-sidebar",
time: { created: 1700000000000, updated: 1700000000000 },
sandboxes: [],
},
provider: {
all: [
{
id: "opencode",
name: "OpenCode",
models: { test: { id: "test", name: "Test", limit: { context: 200_000 } } },
},
],
connected: ["opencode"],
default: { providerID: "opencode", modelID: "test" },
},
sessions: [
{
id: sessionID,
slug: sessionID,
projectID,
directory,
title,
version: "dev",
time: { created: 1700000000000, updated: 1700000000000 },
},
],
vcsDiff: [],
fileList: (path) => {
if (path) return []
return files.map((name) => ({
name,
path: name,
absolute: `${directory}/${name}`,
type: "file" as const,
ignored: false,
}))
},
fileContent: (path) => ({ type: "text", content: `contents:${path}` }),
pageMessages: () => ({ items: [] }),
})
await page.addInitScript(
({ directory, server, sessionID }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem(
"opencode.global.dat:server",
JSON.stringify({
projects: { local: [{ worktree: directory, expanded: true }] },
lastProject: { local: directory },
}),
)
localStorage.setItem(
"opencode.global.dat:layout",
JSON.stringify({ review: { diffStyle: "split", panelOpened: true } }),
)
localStorage.setItem(
"opencode.global.dat:review-panel-v2",
JSON.stringify({ sidebarOpened: true, sidebarWidth: 240, expandMode: "collapse" }),
)
localStorage.setItem(
"opencode.window.browser.dat:tabs",
JSON.stringify([{ type: "session", server, sessionId: sessionID }]),
)
},
{ directory, server, sessionID },
)
}
@@ -0,0 +1,40 @@
import { expect, test } from "@playwright/test"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { mockOpenCodeServer } from "../utils/mock-server"
const draftID = "draft_legacy_new_session"
const directory = "C:/OpenCode/LegacyNewSession"
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
test("redirects a draft to the legacy new-session route", async ({ page }) => {
await mockOpenCodeServer(page, {
directory,
project: {
id: "proj_legacy_new_session",
worktree: directory,
vcs: "git",
name: "legacy-new-session",
time: { created: 1700000000000, updated: 1700000000000 },
sandboxes: [],
},
provider: { all: [], connected: [], default: {} },
sessions: [],
pageMessages: () => ({ items: [] }),
})
await page.addInitScript(
({ directory, draftID, server }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: false } }))
localStorage.setItem(
"opencode.window.browser.dat:tabs",
JSON.stringify([{ type: "draft", draftID, server, directory }]),
)
},
{ directory, draftID, server },
)
await page.goto(`/new-session?draftId=${draftID}`)
await expect(page).toHaveURL(`/${base64Encode(directory)}/session`)
await expect(page.locator("header[data-tauri-drag-region]")).toBeVisible()
await expect(page.locator('[data-component="prompt-input"]')).toBeVisible()
})
@@ -0,0 +1,83 @@
import { expect, test } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectAppVisible } from "../utils/waits"
const draftID = "draft_new_session_panel_corner"
const directory = "C:/OpenCode/NewSessionPanelCorner"
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
test.use({
viewport: { width: 935, height: 522 },
deviceScaleFactor: 1,
})
test("matches the rounded panel corners to the dark new-session background", async ({ page }, testInfo) => {
await mockOpenCodeServer(page, {
directory,
project: {
id: "proj_new_session_panel_corner",
worktree: directory,
vcs: "git",
name: "new-session-panel-corner",
time: { created: 1700000000000, updated: 1700000000000 },
sandboxes: [],
},
provider: { all: [], connected: [], default: {} },
sessions: [],
pageMessages: () => ({ items: [] }),
})
await page.addInitScript(
({ directory, draftID, server }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem("opencode-theme-id", "oc-2")
localStorage.setItem("opencode-color-scheme", "dark")
localStorage.setItem(
"opencode.global.dat:server",
JSON.stringify({
projects: { local: [{ worktree: directory, expanded: true }] },
lastProject: { local: directory },
}),
)
localStorage.setItem(
"opencode.window.browser.dat:tabs",
JSON.stringify([{ type: "draft", draftID, server, directory }]),
)
},
{ directory, draftID, server },
)
await page.goto(`/new-session?draftId=${draftID}`)
await expectAppVisible(page.locator('[data-component="prompt-input"]'))
await expect(page.locator("html")).toHaveAttribute("data-color-scheme", "dark")
const panel = page.locator('main div[class*="rounded-[10px]"][class*="overflow-hidden"]')
await expect(panel).toHaveCount(1)
const box = await panel.boundingBox()
if (!box) throw new Error("New-session panel bounds are unavailable")
const screenshot = await page.screenshot({ path: testInfo.outputPath("new-session-dark.png") })
const corners = await page.evaluate(
async ({ source, points }) => {
const image = new Image()
image.src = source
await image.decode()
const canvas = document.createElement("canvas")
canvas.width = image.naturalWidth
canvas.height = image.naturalHeight
const context = canvas.getContext("2d")
if (!context) throw new Error("2D canvas is unavailable")
context.drawImage(image, 0, 0)
return points.map((point) => Array.from(context.getImageData(point.x, point.y, 1, 1).data))
},
{
source: `data:image/png;base64,${screenshot.toString("base64")}`,
points: [
{ x: Math.floor(box.x), y: Math.floor(box.y) },
{ x: Math.ceil(box.x + box.width) - 1, y: Math.floor(box.y) },
{ x: Math.floor(box.x), y: Math.ceil(box.y + box.height) - 1 },
{ x: Math.ceil(box.x + box.width) - 1, y: Math.ceil(box.y + box.height) - 1 },
],
},
)
expect(corners.every(([red, green, blue, alpha]) => red <= 8 && green <= 8 && blue <= 8 && alpha === 255)).toBe(true)
})
@@ -66,7 +66,7 @@ test("shows the V2 thinking level control while relevant", async ({ page }) => {
await expect(control).toBeVisible()
await control.locator('[data-action="prompt-model-variant"]').click()
const high = page.getByRole("option", { name: "high" })
const high = page.getByRole("menuitemradio", { name: "high" })
await expect(high).toBeVisible()
await page.mouse.move(0, 0)
await expect(control).toBeVisible()
@@ -0,0 +1,109 @@
import { expect, test, type Page, type Route } from "@playwright/test"
import { base64Encode } from "@opencode-ai/core/util/encode"
const serverA = "http://127.0.0.1:4096"
const serverB = "http://127.0.0.1:4097"
const sessionA = session("ses_server_a", "C:/server-a", "Server A session")
const sessionB = session("ses_server_b", "/home/server-b", "Server B session")
test("tab busy indicator reflects the tab server's own session status", async ({ page }) => {
await mockServers(page)
await page.addInitScript(
({ serverA, serverB, sessionA, sessionB }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] }))
localStorage.setItem(
"opencode.window.browser.dat:tabs",
JSON.stringify([
{ type: "session", server: serverA, sessionId: sessionA },
{ type: "session", server: serverB, sessionId: sessionB },
]),
)
},
{ serverA, serverB, sessionA: sessionA.id, sessionB: sessionB.id },
)
const hrefA = `/server/${base64Encode(serverA)}/session/${sessionA.id}`
const hrefB = `/server/${base64Encode(serverB)}/session/${sessionB.id}`
await page.goto(hrefA)
await expect(page.getByText(sessionA.title).first()).toBeVisible()
// Session B is busy on server B while server A stays the active server, so the
// busy indicator must come from the tab server's status, not the active server's.
const tabB = page.locator(`[data-titlebar-tab-slot]:has(a[href="${hrefB}"])`)
await expect(tabB.locator('[data-component="session-progress-indicator-v2"]')).toBeVisible()
const tabA = page.locator(`[data-titlebar-tab-slot]:has(a[href="${hrefA}"])`)
await expect(tabA.locator("[data-titlebar-tab-title]")).toHaveText(sessionA.title)
await expect(tabA.locator('[data-component="session-progress-indicator-v2"]')).toHaveCount(0)
})
function session(id: string, directory: string, title: string) {
return {
id,
slug: id,
projectID: `project-${id}`,
directory,
title,
version: "dev",
time: { created: 1, updated: 1 },
}
}
async function mockServers(page: Page) {
await page.route("**/*", async (route) => {
const url = new URL(route.request().url())
if (url.origin !== serverA && url.origin !== serverB) return route.fallback()
const current = url.origin === serverA ? sessionA : sessionB
const directory = url.searchParams.get("directory")
if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500)
if (url.pathname === "/global/event" || url.pathname === "/event") return sse(route)
if (url.pathname === "/global/health") return json(route, { healthy: true })
if (url.pathname === "/session/status")
return json(route, url.origin === serverB ? { [sessionB.id]: { type: "busy" } } : {})
if (url.pathname === "/session") return json(route, [current])
if (url.pathname === `/session/${current.id}`) return json(route, current)
if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404)
if (url.pathname === `/session/${current.id}/message`) return json(route, [])
if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(url.pathname)) return json(route, [])
if (["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/diff"].includes(url.pathname))
return json(route, [])
if (["/global/config", "/config", "/provider/auth", "/mcp"].includes(url.pathname)) return json(route, {})
if (url.pathname === "/provider")
return json(route, { all: [], connected: [], default: { providerID: "", modelID: "" } })
if (url.pathname === "/agent") return json(route, [{ name: "build", mode: "primary" }])
if (url.pathname === "/project" || url.pathname === "/project/current") {
const project = {
id: current.projectID,
worktree: current.directory,
vcs: "git",
time: { created: 1, updated: 1 },
sandboxes: [],
}
return json(route, url.pathname === "/project" ? [project] : project)
}
if (url.pathname === "/path")
return json(route, {
state: current.directory,
config: current.directory,
worktree: current.directory,
directory: current.directory,
home: current.directory,
})
if (url.pathname === "/vcs") return json(route, { branch: "main", default_branch: "main" })
return json(route, {})
})
}
function json(route: Route, body: unknown, status = 200) {
return route.fulfill({
status,
contentType: "application/json",
headers: { "access-control-allow-origin": "*" },
body: JSON.stringify(body),
})
}
function sse(route: Route) {
return route.fulfill({ status: 200, contentType: "text/event-stream", body: ": ok\n\n" })
}
@@ -0,0 +1,202 @@
import { expect, test, type Page } from "@playwright/test"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectAppVisible, expectSessionTitle } from "../utils/waits"
const directory = "C:/OpenCode/ReviewImageFlashRegression"
const sessionID = "ses_review_image_flash_regression"
const title = "Review image flash regression"
const imageFile = "assets/preview.png"
test("clicking an image file in the v2 review pane does not blank the panel", async ({ page }) => {
await openReview(page)
await installReviewFlashProbe(page)
await page.getByRole("button", { name: /preview\.png/ }).click()
await waitForReviewFlashProbe(page, 400)
const trace = await collectReviewFlashProbe(page)
const bad = trace.samples.filter((sample) => sample.blank || sample.blackCenter)
expect(trace.samples.length).toBeGreaterThan(0)
expect(
bad,
JSON.stringify({ bad: bad.slice(0, 8), first: trace.samples.slice(0, 8), last: trace.samples.slice(-4) }, null, 2),
).toEqual([])
})
async function openReview(page: Page) {
await page.setViewportSize({ width: 960, height: 900 })
await page.addInitScript(() => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
})
await mockOpenCodeServer(page, {
directory,
project: {
id: "proj_review_image_flash_regression",
worktree: directory,
vcs: "git",
name: "review-image-flash-regression",
time: { created: 1700000000000, updated: 1700000000000 },
sandboxes: [],
},
provider: { all: [], connected: [], default: {} },
sessions: [
{
id: sessionID,
slug: "review-image-flash-regression",
projectID: "proj_review_image_flash_regression",
directory,
title,
version: "dev",
time: { created: 1700000000000, updated: 1700000000000 },
},
],
vcsDiff: [
{
file: "src/example.ts",
additions: 1,
deletions: 1,
status: "modified",
patch:
"diff --git a/src/example.ts b/src/example.ts\n--- a/src/example.ts\n+++ b/src/example.ts\n@@ -1 +1 @@\n-export const value = 'before'\n+export const value = 'after'\n",
},
{
file: imageFile,
patch: "",
additions: 1,
deletions: 0,
status: "added",
},
],
fileContent: async (path) => {
if (path !== imageFile) return undefined
await new Promise((resolve) => setTimeout(resolve, 250))
return {
type: "binary",
content: "iVBORw0KGgo=",
encoding: "base64",
mimeType: "image/png",
}
},
fileList: (path) => {
if (!path) {
return [
{ name: "assets", path: "assets", absolute: `${directory}/assets`, type: "directory", ignored: false },
{ name: "src", path: "src", absolute: `${directory}/src`, type: "directory", ignored: false },
]
}
if (path === "assets") {
return [
{
name: "preview.png",
path: imageFile,
absolute: `${directory}/${imageFile}`,
type: "file",
ignored: false,
},
]
}
if (path === "src") {
return [
{
name: "example.ts",
path: "src/example.ts",
absolute: `${directory}/src/example.ts`,
type: "file",
ignored: false,
},
]
}
return []
},
pageMessages: () => ({
items: [
{
info: {
id: "msg_review_image_flash_regression",
sessionID,
role: "user",
time: { created: 1700000000000 },
summary: { diffs: [] },
agent: "build",
model: { providerID: "opencode", modelID: "test" },
},
parts: [
{
id: "prt_review_image_flash_regression",
sessionID,
messageID: "msg_review_image_flash_regression",
type: "text",
text: "Review this change.",
},
],
},
],
}),
})
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
await expectSessionTitle(page, title)
await page.getByRole("button", { name: "Toggle review" }).click()
await expectAppVisible(page.locator('#review-panel [data-component="session-review-v2"]'))
await expectAppVisible(page.getByRole("button", { name: /preview\.png/ }))
}
async function installReviewFlashProbe(page: Page) {
await page.evaluate(() => {
const samples: Array<{
observedAtMs: number
blank: boolean
blackCenter: boolean
text: string
background: string
}> = []
const startedAt = performance.now()
const sample = () => {
const panel = document.querySelector<HTMLElement>('#review-panel [data-component="session-review-v2"]')
const rect = panel?.getBoundingClientRect()
const center = rect
? document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2)
: undefined
const background = center instanceof Element ? getComputedStyle(center).backgroundColor : ""
samples.push({
observedAtMs: performance.now() - startedAt,
blank: !panel || panel.textContent?.trim().length === 0,
blackCenter: background === "rgb(0, 0, 0)",
text: panel?.textContent?.trim().slice(0, 80) ?? "",
background,
})
if (performance.now() - startedAt < 500) requestAnimationFrame(sample)
}
document.addEventListener(
"click",
(event) => {
const target = event.target instanceof Element ? event.target : undefined
if (!target?.closest('[data-slot="file-tree-v2-row"]')) return
requestAnimationFrame(sample)
},
{ capture: true, once: true },
)
;(window as Window & { __reviewImageFlash?: { samples: typeof samples; startedAt: number } }).__reviewImageFlash = {
samples,
startedAt,
}
})
}
async function waitForReviewFlashProbe(page: Page, durationMs: number) {
await page.waitForFunction((durationMs) => {
const state = (window as Window & { __reviewImageFlash?: { samples: unknown[]; startedAt: number } })
.__reviewImageFlash
return !!state && state.samples.length > 0 && performance.now() - state.startedAt >= durationMs
}, durationMs)
}
async function collectReviewFlashProbe(page: Page) {
return page.evaluate(() => {
return (window as Window & { __reviewImageFlash?: { samples: unknown[]; startedAt: number } }).__reviewImageFlash!
}) as Promise<{
startedAt: number
samples: Array<{ observedAtMs: number; blank: boolean; blackCenter: boolean; text: string; background: string }>
}>
}
@@ -0,0 +1,163 @@
import { base64Encode } from "@opencode-ai/core/util/encode"
import { expect, test } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectSessionTitle } from "../utils/waits"
const directory = "C:/OpenCode/ReviewOpenFile"
const projectID = "proj_review_open_file"
const sessionID = "ses_review_open_file"
const title = "Review open file"
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
test.use({ viewport: { width: 1440, height: 900 } })
test("opens and searches project files inline", async ({ page }) => {
const searches: { query: string; dirs?: string; limit?: number }[] = []
await mockOpenCodeServer(page, {
directory,
project: {
id: projectID,
worktree: directory,
vcs: "git",
name: "open-file-project",
time: { created: 1700000000000, updated: 1700000000000 },
sandboxes: [],
},
provider: {
all: [
{
id: "opencode",
name: "OpenCode",
models: { test: { id: "test", name: "Test", limit: { context: 200_000 } } },
},
],
connected: ["opencode"],
default: { providerID: "opencode", modelID: "test" },
},
sessions: [
{
id: sessionID,
slug: sessionID,
projectID,
directory,
title,
version: "dev",
time: { created: 1700000000000, updated: 1700000000000 },
},
],
vcsDiff: [fileDiff("src/changed.ts")],
fileList: (path) => {
if (path) return []
return [
fileNode("README.md"),
{ name: "src", path: "src", absolute: `${directory}/src`, type: "directory", ignored: false },
]
},
fileContent: (path) => ({ type: "text", content: `contents:${path}` }),
findFiles: (input) => {
searches.push(input)
return input.query === "nested" ? ["src/nested.ts"] : []
},
pageMessages: () => ({ items: [] }),
})
await page.addInitScript(
({ directory, server, sessionID }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem(
"opencode.global.dat:server",
JSON.stringify({
projects: { local: [{ worktree: directory, expanded: true }] },
lastProject: { local: directory },
}),
)
localStorage.setItem(
"opencode.global.dat:layout",
JSON.stringify({ review: { diffStyle: "split", panelOpened: true } }),
)
localStorage.setItem(
"opencode.global.dat:review-panel-v2",
JSON.stringify({ sidebarOpened: false, sidebarWidth: 240, expandMode: "collapse" }),
)
localStorage.setItem(
"opencode.window.browser.dat:tabs",
JSON.stringify([{ type: "session", server, sessionId: sessionID }]),
)
},
{ directory, server, sessionID },
)
await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`)
await expectSessionTitle(page, title)
const panel = page.locator("#review-panel")
const sidebar = panel.locator('[data-slot="session-review-v2-sidebar"]')
const sidebarToggle = panel.getByRole("button", { name: "Toggle file tree" })
const contextButton = page.getByRole("button", { name: "View context usage" })
await contextButton.click()
await expect(panel.getByRole("tab", { name: "Context" })).toHaveAttribute("data-selected", "")
await panel.getByRole("button", { name: "Open file" }).click()
await expect(panel.getByRole("tab", { name: "Open file" })).toHaveAttribute("data-selected", "")
await expect(sidebarToggle).toBeDisabled()
await expect(sidebar).toBeVisible()
await contextButton.click()
await expect(panel.getByRole("tab", { name: "Context" })).toHaveAttribute("data-selected", "")
await expect(sidebar).toBeHidden()
await panel.getByRole("button", { name: "Open file" }).click()
const filter = panel.getByRole("combobox", { name: "Filter files" })
await expect(filter).toBeFocused()
await expect(panel.getByRole("tab", { name: "Open file" })).toHaveAttribute("data-selected", "")
await expect(panel.getByText("open-file-project", { exact: true })).toBeVisible()
await panel.getByRole("button", { name: "README.md" }).click()
await expect(panel.getByRole("tab", { name: "README.md" })).toHaveAttribute("data-selected", "")
await expect(sidebarToggle).toBeEnabled()
await expect(panel.getByText("contents:README.md", { exact: true })).toBeVisible()
await expect(sidebar).toHaveCount(0)
await panel.getByRole("button", { name: "Open file" }).click()
await expect(panel.getByRole("tab", { name: "README.md" })).toHaveCount(0)
await expect(sidebar).toBeVisible()
await filter.fill("nested")
const result = panel.getByRole("option", { name: /nested\.ts/ })
await expect(result).toBeVisible()
const resultID = await result.getAttribute("id")
expect(resultID).toBeTruthy()
await expect(filter).toHaveAttribute("aria-activedescendant", resultID!)
await filter.press("Enter")
await expect(panel.getByRole("tab", { name: "nested.ts" })).toHaveAttribute("data-selected", "")
await expect(sidebarToggle).toBeEnabled()
await expect(panel.getByText("contents:src/nested.ts", { exact: true })).toBeVisible()
expect(searches).toContainEqual({ query: "nested", dirs: "false", limit: 200 })
await panel.getByRole("button", { name: "Open file" }).click()
await expect(panel.getByRole("tab", { name: "nested.ts" })).toHaveCount(1)
await expect(panel.getByRole("tab", { name: "Open file" })).toHaveAttribute("data-selected", "")
await expect(sidebarToggle).toBeDisabled()
await panel.getByRole("tab", { name: /Review/ }).click()
await expect(sidebarToggle).toBeEnabled()
await panel.getByRole("tab", { name: "Open file" }).click()
await page.keyboard.press("Control+w")
await expect(panel.getByRole("tab", { name: "Open file" })).toHaveCount(0)
await expect(panel.getByRole("tab", { name: "nested.ts" })).toHaveAttribute("data-selected", "")
})
function fileNode(path: string) {
return {
name: path,
path,
absolute: `${directory}/${path}`,
type: "file",
ignored: false,
}
}
function fileDiff(file: string) {
return {
file,
before: "before\n",
after: "after\n",
additions: 1,
deletions: 1,
status: "modified",
}
}
@@ -0,0 +1,152 @@
import { base64Encode } from "@opencode-ai/core/util/encode"
import { expect, test, type Page } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectSessionTitle } from "../utils/waits"
const directory = "C:/OpenCode/ReviewStatePersistence"
const projectID = "proj_review_state_persistence"
const sessionA = "ses_review_state_a"
const sessionB = "ses_review_state_b"
const titleA = "Alpha review state"
const titleB = "Beta review state"
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
test.use({ viewport: { width: 1440, height: 900 } })
test("restores review mode and selected file per session", async ({ page }) => {
await setup(page)
await page.goto(sessionHref(sessionA))
await expectSessionTitle(page, titleA)
await page.getByRole("button", { name: "Toggle review" }).click()
await selectMode(page, "Git changes", "Branch changes")
await selectFile(page, "beta.ts")
await switchSession(page, titleB)
await expect(page.getByRole("button", { name: "Git changes" })).toBeVisible()
await selectFile(page, "gamma.ts")
await switchSession(page, titleA)
await expect(page.getByRole("button", { name: "Branch changes" })).toBeVisible()
await expectSelectedFile(page, "beta.ts")
await selectMode(page, "Branch changes", "Git changes")
await expectSelectedFile(page, "alpha.ts")
await selectMode(page, "Git changes", "Branch changes")
await expectSelectedFile(page, "beta.ts")
await page.reload()
await expectSessionTitle(page, titleA)
await expect(page.getByRole("button", { name: "Branch changes" })).toBeVisible()
await expectSelectedFile(page, "beta.ts")
await switchSession(page, titleB)
await expect(page.getByRole("button", { name: "Git changes" })).toBeVisible()
await expectSelectedFile(page, "gamma.ts")
})
async function selectMode(page: Page, current: string, next: string) {
await page.getByRole("button", { name: current }).click()
await page.getByRole("option", { name: next }).click()
}
async function selectFile(page: Page, file: string) {
await page.getByRole("button", { name: file }).click()
await expectSelectedFile(page, file)
}
async function expectSelectedFile(page: Page, file: string) {
await expect(page.locator('[data-slot="session-review-v2-file-name"]')).toHaveText(file)
}
async function switchSession(page: Page, title: string) {
await page.locator("[data-titlebar-tab-slot]", { hasText: title }).click()
await expectSessionTitle(page, title)
}
async function setup(page: Page) {
await mockOpenCodeServer(page, {
directory,
project: {
id: projectID,
worktree: directory,
vcs: "git",
name: "review-state-persistence",
time: { created: 1700000000000, updated: 1700000000000 },
sandboxes: [],
},
provider: {
all: [
{
id: "opencode",
name: "OpenCode",
models: { test: { id: "test", name: "Test", limit: { context: 200_000 } } },
},
],
connected: ["opencode"],
default: { providerID: "opencode", modelID: "test" },
},
sessions: [session(sessionA, titleA, 1700000000000), session(sessionB, titleB, 1700000001000)],
pageMessages: () => ({ items: [] }),
})
await page.route(/\/vcs(?:\?.*)?$/, (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ branch: "feature", default_branch: "dev" }),
}),
)
await page.route("**/vcs/diff**", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(
new URL(route.request().url()).searchParams.get("mode") === "branch"
? [diff("src/alpha.ts"), diff("src/beta.ts")]
: [diff("src/alpha.ts"), diff("src/gamma.ts")],
),
}),
)
await page.addInitScript(
({ directory, server, sessions }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem(
"opencode.global.dat:server",
JSON.stringify({
projects: { local: [{ worktree: directory, expanded: true }] },
lastProject: { local: directory },
}),
)
localStorage.setItem(
"opencode.window.browser.dat:tabs",
JSON.stringify(sessions.map((sessionId: string) => ({ type: "session", server, sessionId }))),
)
},
{ directory, server, sessions: [sessionA, sessionB] },
)
}
function session(id: string, title: string, created: number) {
return {
id,
slug: id,
projectID,
directory,
title,
version: "dev",
time: { created, updated: created },
}
}
function diff(file: string) {
return {
file,
additions: 1,
deletions: 1,
status: "modified",
patch: `diff --git a/${file} b/${file}\n--- a/${file}\n+++ b/${file}\n@@ -1 +1 @@\n-export const value = 'before'\n+export const value = 'after'\n`,
}
}
function sessionHref(sessionID: string) {
return `/server/${base64Encode(server)}/session/${sessionID}`
}
@@ -0,0 +1,147 @@
import { base64Encode } from "@opencode-ai/core/util/encode"
import { expect, test, type Page } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectAppVisible, expectSessionTitle } from "../utils/waits"
const directory = "C:/OpenCode/ReviewTabSwitch"
const projectID = "proj_review_tab_switch"
const sessionA = "ses_review_tab_a"
const sessionB = "ses_review_tab_b"
const titleA = "Alpha session"
const titleB = "Beta session"
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
const diffs = Array.from({ length: 2_740 }, (_, index) =>
fileDiff(`src/generated-${String(index).padStart(4, "0")}.ts`),
)
// Marks the review pane DOM node so a remount (fresh node) is detectable.
const PROBE = "original"
test.use({ viewport: { width: 1440, height: 900 } })
// The v2 review pane's diff data is workspace-scoped: switching between session
// tabs in the same workspace must update its parameters reactively instead of
// tearing the pane down and remounting it (which flickers).
test("keeps the v2 review pane mounted when switching session tabs in a workspace", async ({ page }) => {
await setup(page)
await page.goto(sessionHref(sessionA))
await expectSessionTitle(page, titleA)
await page.getByRole("button", { name: "Toggle review" }).click()
const reviewTab = page.locator("#session-side-panel-review-tab")
const reviewTabPanel = page.locator("#session-side-panel-review-tabpanel")
await expect(reviewTab).toHaveAttribute("aria-controls", "session-side-panel-review-tabpanel")
await expect(reviewTabPanel).toHaveAttribute("id", "session-side-panel-review-tabpanel")
const review = page.locator('#review-panel [data-component="session-review-v2"]')
await expectAppVisible(review)
await expectAppVisible(page.getByRole("button", { name: "generated-0000.ts" }))
await writeProbe(page)
await switchTab(page, titleB)
await expectSessionTitle(page, titleB)
await expectAppVisible(review)
await expectAppVisible(page.getByRole("button", { name: "generated-0000.ts" }))
expect(await readProbe(page)).toBe(PROBE)
await switchTab(page, titleA)
await expectSessionTitle(page, titleA)
await expectAppVisible(review)
await expectAppVisible(page.getByRole("button", { name: "generated-0000.ts" }))
expect(await readProbe(page)).toBe(PROBE)
const viewport = page.locator('#review-panel [data-slot="session-review-v2-sidebar-tree"] .scroll-view__viewport')
await viewport.hover()
await page.mouse.wheel(0, 100_000)
await expect
.poll(() => viewport.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop))
.toBeLessThanOrEqual(1)
await expect(page.getByRole("button", { name: "generated-2739.ts" })).toBeVisible()
})
type Probed = HTMLElement & { __e2eProbe?: string }
async function switchTab(page: Page, title: string) {
await page.locator("[data-titlebar-tab-slot]", { hasText: title }).click()
}
async function writeProbe(page: Page) {
await page.locator('#review-panel [data-component="session-review-v2"]').evaluate((el, probe) => {
;(el as Probed).__e2eProbe = probe
}, PROBE)
}
async function readProbe(page: Page) {
return page.locator('#review-panel [data-component="session-review-v2"]').evaluate((el) => (el as Probed).__e2eProbe)
}
async function setup(page: Page) {
await mockOpenCodeServer(page, {
directory,
project: {
id: projectID,
worktree: directory,
vcs: "git",
name: "review-tab-switch",
time: { created: 1700000000000, updated: 1700000000000 },
sandboxes: [],
},
provider: {
all: [
{
id: "opencode",
name: "OpenCode",
models: { test: { id: "test", name: "Test", limit: { context: 200_000 } } },
},
],
connected: ["opencode"],
default: { providerID: "opencode", modelID: "test" },
},
sessions: [session(sessionA, titleA, 1700000000000), session(sessionB, titleB, 1700000001000)],
vcsDiff: diffs,
pageMessages: () => ({ items: [] }),
})
await page.addInitScript(
({ directory, server, sessions }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem(
"opencode.global.dat:server",
JSON.stringify({
projects: { local: [{ worktree: directory, expanded: true }] },
lastProject: { local: directory },
}),
)
localStorage.setItem(
"opencode.window.browser.dat:tabs",
JSON.stringify(sessions.map((sessionId: string) => ({ type: "session", server, sessionId }))),
)
},
{ directory, server, sessions: [sessionA, sessionB] },
)
}
function session(id: string, title: string, created: number) {
return {
id,
slug: id,
projectID,
directory,
title,
version: "dev",
time: { created, updated: created },
}
}
function sessionHref(sessionID: string) {
return `/server/${base64Encode(server)}/session/${sessionID}`
}
function fileDiff(file: string) {
return {
file,
additions: 1,
deletions: 1,
status: "modified",
patch: `diff --git a/${file} b/${file}\n--- a/${file}\n+++ b/${file}\n@@ -1 +1 @@\n-export const value = 'before'\n+export const value = 'after'\n`,
}
}
@@ -0,0 +1,263 @@
import { expect, test, type Page } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectSessionTitle } from "../utils/waits"
const directory = "C:/OpenCode/ReviewTerminalStacked"
const projectID = "proj_review_terminal_stacked"
const sessionID = "ses_review_terminal_stacked"
const title = "Review terminal stacked"
const branchDiffs = [
fileDiff(".github/actions/setup-bun/action.yml", 7),
...Array.from({ length: 2_739 }, (_, index) =>
fileDiff(
`src/branch/d${String(Math.floor(index / 100)).padStart(5, "0")}/generated-${String(index).padStart(4, "0")}.ts`,
100,
false,
),
),
]
test("keeps the review tree and terminal sized when both panels are open", async ({ page }) => {
test.setTimeout(120_000)
const events: Array<{ directory: string; payload: Record<string, unknown> }> = []
let detailVersion = 1
let detailFailures = 1
await page.setViewportSize({ width: 1400, height: 900 })
await mockOpenCodeServer(page, {
directory,
project: {
id: projectID,
worktree: directory,
vcs: "git",
name: "review-terminal-stacked",
time: { created: 1700000000000, updated: 1700000000000 },
sandboxes: [],
},
provider: {
all: [
{
id: "opencode",
name: "OpenCode",
models: { test: { id: "test", name: "Test", limit: { context: 200_000 } } },
},
],
connected: ["opencode"],
default: { providerID: "opencode", modelID: "test" },
},
sessions: [
{
id: sessionID,
slug: "review-terminal-stacked",
projectID,
directory,
title,
version: "dev",
time: { created: 1700000000000, updated: 1700000000000 },
},
],
sessionStatus: { [sessionID]: { type: "idle" } },
pageMessages: () => ({ items: [] }),
events: () => events.splice(0, 1),
eventRetry: 16,
})
await page.route(/\/vcs(?:\?.*)?$/, (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ branch: "review-pane-performance", default_branch: "dev" }),
}),
)
await page.route("**/vcs/diff**", (route) => {
const url = new URL(route.request().url())
const scope = url.searchParams.get("directory")?.replaceAll("\\", "/")
const detail = scope?.endsWith("/src/branch/d00027")
if (detail && detailFailures-- > 0) return route.fulfill({ status: 500, body: "retry detail" })
return route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(
url.searchParams.get("mode") === "branch"
? detail
? branchDiffs
.filter((diff) => diff.file.startsWith("src/branch/d00027/"))
.map((diff) => fileDiff(diff.file, diff.additions, true, detailVersion))
: branchDiffs
: Array.from({ length: 7 }, (_, index) => fileDiff(`src/git-${index}.ts`, 1)),
),
})
})
await page.route("**/pty", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ id: "pty_review_terminal", title: "Terminal 1" }),
}),
)
await page.route("**/pty/pty_review_terminal", (route) =>
route.fulfill({ status: 200, contentType: "application/json", body: "{}" }),
)
await page.routeWebSocket("**/pty/pty_review_terminal/connect", () => undefined)
await page.addInitScript(() => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem(
"opencode.global.dat:layout",
JSON.stringify({ review: { diffStyle: "split", panelOpened: true } }),
)
})
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
await expectSessionTitle(page, title)
await expect(page.locator("#review-panel")).toBeVisible()
await expectTree(page, 8, "git-0.ts")
await selectMode(page, "Git changes", "Branch changes")
await expect(page.locator("#session-side-panel-review-tab")).toHaveText("Files Changed 2740")
await page.keyboard.press("Control+Backquote")
await expect(page.locator("#terminal-panel")).toBeVisible()
await expectTree(page, 2_773, "action.yml")
await expectStackGeometry(page)
const treeViewport = page.locator('#review-panel [data-slot="session-review-v2-sidebar-tree"] .scroll-view__viewport')
await treeViewport.hover()
await page.mouse.wheel(0, 100_000)
await expect
.poll(() => treeViewport.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop))
.toBeLessThanOrEqual(1)
const lastFile = page.getByRole("button", { name: "generated-2738.ts" })
await expect(lastFile).toBeVisible()
const bottomGap = await lastFile.evaluate((element) => {
const viewport = element.closest<HTMLElement>(".scroll-view__viewport")!.getBoundingClientRect()
return viewport.bottom - element.getBoundingClientRect().bottom
})
expect(bottomGap).toBeGreaterThanOrEqual(0)
expect(bottomGap).toBeLessThanOrEqual(16)
const lazyDiff = page.waitForRequest((request) => {
const url = new URL(request.url())
return (
url.pathname === "/vcs/diff" &&
url.searchParams.get("directory")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true
)
})
await lastFile.click()
await lazyDiff
const preview = page.locator('[data-slot="session-review-v2-diff-scroll"]')
await expect(preview).toContainText("after-1")
detailVersion = 2
events.push(statusEvent("busy"))
await expect(page.getByRole("button", { name: "Stop" })).toBeVisible()
const refreshedDiff = page.waitForRequest((request) => {
const url = new URL(request.url())
return (
url.pathname === "/vcs/diff" &&
url.searchParams.get("directory")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true
)
})
events.push(statusEvent("idle"))
await refreshedDiff
await expect(preview).toContainText("after-2")
await selectMode(page, "Branch changes", "Git changes")
await expectTree(page, 8, "git-0.ts")
await page.getByRole("button", { name: "git-0.ts" }).click()
await selectMode(page, "Git changes", "Branch changes")
await expectTree(page, 2_773, "action.yml")
const filter = page.getByRole("searchbox", { name: "Filter files" })
await filter.fill("generated-2738")
await expectTree(page, 1, "generated-2738.ts")
await filter.fill("")
await expectTree(page, 2_773, "action.yml")
await page.getByRole("button", { name: "Toggle file tree" }).click()
await expect(page.locator('[data-slot="session-review-v2-sidebar"]')).toHaveCount(0)
await expect(page.locator('#review-panel [data-component="file-tree-v2"]')).toHaveCount(0)
await page.getByRole("button", { name: "Toggle file tree" }).click()
await expectTree(page, 2_773, "action.yml")
await page.keyboard.press("Control+Backquote")
await expect(page.locator("#terminal-panel")).toHaveCount(0)
await expectTree(page, 2_773, "action.yml")
await page.keyboard.press("Control+Backquote")
await expect(page.locator("#terminal-panel")).toBeVisible()
await expectTree(page, 2_773, "action.yml")
await page.getByRole("button", { name: "Toggle review" }).click()
await expect(page.locator("#review-panel")).toHaveCount(0)
await page.getByRole("button", { name: "Toggle review" }).click()
await expectTree(page, 2_773, "action.yml")
await page.setViewportSize({ width: 1_000, height: 700 })
await expectTree(page, 2_773, "action.yml")
await expectStackGeometry(page)
await page.setViewportSize({ width: 1_000, height: 120 })
await page.setViewportSize({ width: 1_400, height: 900 })
await expectTree(page, 2_773, "action.yml")
await expectStackGeometry(page)
})
async function selectMode(page: Page, current: string, next: string) {
await page.getByRole("button", { name: current }).click()
const option = page.getByRole("option", { name: next })
await expect(option).toBeVisible()
await option.click()
}
async function expectTree(page: Page, total: number, file: string) {
await expectMountedTree(page, total)
await expect(page.getByRole("button", { name: file })).toBeVisible()
}
async function expectMountedTree(page: Page, total: number) {
const tree = page.locator('#review-panel [data-component="file-tree-v2"]')
await expect(tree).toHaveAttribute("data-total-rows", String(total))
await expect
.poll(() => tree.evaluate((element) => element.querySelectorAll('[data-slot="file-tree-v2-row"]').length))
.toBeGreaterThan(0)
const state = await tree.evaluate((element) => ({
root: element.getBoundingClientRect().height,
viewport: element.closest<HTMLElement>(".scroll-view__viewport")!.getBoundingClientRect().height,
rows: element.querySelectorAll('[data-slot="file-tree-v2-row"]').length,
}))
expect(state.viewport).toBeGreaterThan(0)
expect(state.root).toBeGreaterThan(0)
expect(state.rows).toBeGreaterThan(0)
expect(state.rows).toBeLessThanOrEqual(60)
}
async function expectStackGeometry(page: Page) {
const geometry = await page.evaluate(() => {
const review = document.querySelector<HTMLElement>("#review-panel")!
const terminal = document.querySelector<HTMLElement>("#terminal-panel")!
const reviewParent = review.parentElement!.getBoundingClientRect()
const terminalParent = terminal.parentElement!.getBoundingClientRect()
return {
review: review.getBoundingClientRect().height,
reviewParent: reviewParent.height,
terminal: terminal.getBoundingClientRect().height,
terminalParent: terminalParent.height,
}
})
expect(Math.abs(geometry.review - geometry.reviewParent)).toBeLessThanOrEqual(1)
expect(Math.abs(geometry.terminal - geometry.terminalParent)).toBeLessThanOrEqual(1)
}
function base64Encode(value: string) {
return Buffer.from(value, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
}
function statusEvent(type: "busy" | "idle") {
return {
directory,
payload: { type: "session.status", properties: { sessionID, status: { type } } },
}
}
function fileDiff(file: string, additions: number, loaded = true, version = 1) {
return {
file,
additions,
deletions: 0,
status: "modified",
patch: loaded
? `diff --git a/${file} b/${file}\n--- a/${file}\n+++ b/${file}\n@@ -1 +1 @@\n-export const value = 'before'\n+export const value = 'after-${version}'\n`
: `diff --git a/${file} b/${file}\n--- a/${file}\n+++ b/${file}`,
}
}
@@ -38,6 +38,29 @@ test("shows a pending question dock", async ({ page }) => {
await expect(question.getByRole("radio", { name: /Extended/ })).toBeVisible()
await expect(page.locator('[data-component="session-composer"]')).toHaveCount(0)
const rejectRequests: string[] = []
page.on("request", (request) => {
if (request.method() !== "POST") return
if (new URL(request.url()).pathname === "/question/question-request/reject") rejectRequests.push(request.url())
})
await question.locator('[data-component="icon-button"][data-icon="chevron-down"]').click()
await expect(question).toBeVisible()
await expect(question.getByText("Which implementation should be used?")).toBeVisible()
await expect(question.getByText("Select one answer")).toBeHidden()
await expect(question.getByRole("radio", { name: /Minimal/ })).toBeHidden()
await expect(question.getByRole("radio", { name: /Extended/ })).toBeHidden()
await expect(question.getByRole("button", { name: "Dismiss" })).toBeVisible()
await expect(question.getByRole("button", { name: "Submit" })).toBeVisible()
await expect(page.locator('[data-component="question-minimized-dock"]')).toHaveCount(0)
expect(rejectRequests).toEqual([])
await question.locator('[data-component="icon-button"][data-icon="chevron-down"]').click()
await expect(question).toBeVisible()
await expect(question.getByText("Which implementation should be used?")).toBeVisible()
await expect(question.getByRole("radio", { name: /Minimal/ })).toBeVisible()
expect(rejectRequests).toEqual([])
await question.getByRole("radio", { name: /Minimal/ }).click()
const reply = page.waitForRequest(
(request) => request.method() === "POST" && new URL(request.url()).pathname === "/question/question-request/reply",
@@ -0,0 +1,22 @@
import { expect, test } from "@playwright/test"
import { assistantMessage, setupTimeline, shell, userMessage } from "../performance/timeline-stability/fixture"
test("space activates a focused timeline button instead of scrolling", async ({ page }) => {
const shellID = "prt_space_button_shell"
await setupTimeline(page, {
messages: [userMessage(), assistantMessage([shell(shellID, "completed", lines(5))])],
settings: { shellToolPartsExpanded: false },
reducedMotion: true,
})
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
const trigger = page.locator(`[data-timeline-part-id="${shellID}"] [data-slot="collapsible-trigger"]`)
await trigger.focus()
const before = await scroller.evaluate((element) => element.scrollTop)
await trigger.press("Space")
await expect(trigger).toHaveAttribute("aria-expanded", "true")
expect(await scroller.evaluate((element) => element.scrollTop)).toBe(before)
})
function lines(count: number) {
return Array.from({ length: count }, (_, index) => `line ${index + 1}`).join("\n")
}
@@ -1,6 +1,13 @@
import { expect, test, type Page } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectAppVisible, expectSessionTitle } from "../utils/waits"
import {
analyzeVisualObservations,
defineVisualRegions,
startVisualProbe,
stopVisualProbe,
visualPlan,
} from "../utils/visual-stability"
const directory = "C:/OpenCode/ContextResizeRegression"
const projectID = "proj_context_resize_regression"
@@ -36,6 +43,82 @@ test.describe("regression: session timeline context group resize", () => {
expect(visibleOverlap).toEqual([])
expect(samples.at(-1)?.expanded).toBe("true")
})
test("paints a stable exploring to explored transition", async ({ page }) => {
const events: { directory: string; payload: Record<string, unknown> }[] = []
await page.setViewportSize({ width: 1400, height: 900 })
await mockServer(page, events, [
...Array.from({ length: 8 }, (_, index) => turn(index, false)).flat(),
...turn(10, true, "running"),
])
await configurePage(page)
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
await expectSessionTitle(page, title)
const devtools = await page.context().newCDPSession(page)
await devtools.send("Emulation.setCPUThrottlingRate", { rate: 4 })
const context = page.locator(`[data-timeline-part-ids="${contextIDs.join(",")}"]`).first()
await expectAppVisible(context)
await expect(context.locator('[data-component="tool-status-title"]')).toHaveAttribute("aria-label", "Exploring")
const contextSelector = `[data-timeline-part-ids="${contextIDs.join(",")}"]`
const regions = defineVisualRegions({
status: {
selector: `${contextSelector} [data-component="tool-status-title"]`,
opacitySelectors: ['[data-slot="tool-status-active"]', '[data-slot="tool-status-done"]'],
},
context: { selector: contextSelector, closest: '[data-timeline-row="AssistantPart"]' },
following: {
selector: `[data-timeline-part-id="${followingTextID}"]`,
closest: '[data-timeline-row="AssistantPart"]',
},
})
await startVisualProbe(page, regions)
for (const [index, delay] of [120, 350, 80, 500].entries()) {
events.push({
directory,
payload: {
type: "message.part.updated",
properties: {
part: contextTool(
contextIDs[index]!,
id("msg_assistant", 10),
["read", "glob", "grep", "list"][index]!,
[
{ filePath: "src/recent-a.ts" },
{ path: directory, pattern: "**/*.ts" },
{ path: directory, pattern: "Explored" },
{ path: "src" },
][index]!,
),
},
},
})
await page.waitForTimeout(delay)
}
await expect(context.locator('[data-component="tool-status-title"]')).toHaveAttribute("aria-label", "Explored")
await page.waitForTimeout(700)
const trace = await stopVisualProbe<keyof typeof regions>(page)
const labels = trace.samples
.map((sample) => sample.regions.status?.label)
.filter((value): value is string => !!value)
.filter((value, index, all) => value !== all[index - 1])
const issues = analyzeVisualObservations(
trace.samples,
visualPlan(regions, [
{ type: "required", regions: ["context", "following"] },
{ type: "opacity", regions: "all" },
{ type: "continuity", regions: "all" },
{ type: "motion", regions: "all" },
{ type: "label-stability", regions: "all" },
{ type: "flow", regions: ["context", "following"] },
]),
)
expect(labels).toEqual(["Exploring", "Explored"])
expect(issues, JSON.stringify(trace.samples, null, 2)).toEqual([])
})
})
async function configurePage(page: Page) {
@@ -128,7 +211,7 @@ async function sampleExpansion(page: Page) {
)
}
function turn(index: number, target: boolean): Message[] {
function turn(index: number, target: boolean, status: "running" | "completed" = "completed"): Message[] {
const userID = id("msg_user", index)
const assistantID = id("msg_assistant", index)
return [
@@ -163,10 +246,22 @@ function turn(index: number, target: boolean): Message[] {
},
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" }),
contextTool(
contextIDs[0]!,
assistantID,
"read",
{ filePath: "src/recent-a.ts", offset: 0, limit: 120 },
status,
),
contextTool(contextIDs[1]!, assistantID, "glob", { path: directory, pattern: "**/*.ts" }, status),
contextTool(
contextIDs[2]!,
assistantID,
"grep",
{ path: directory, pattern: "Explored", include: "*.ts" },
status,
),
contextTool(contextIDs[3]!, assistantID, "list", { path: "src" }, status),
{
id: followingTextID,
sessionID,
@@ -188,7 +283,13 @@ function turn(index: number, target: boolean): Message[] {
]
}
function contextTool(partID: string, messageID: string, tool: string, input: Record<string, unknown>) {
function contextTool(
partID: string,
messageID: string,
tool: string,
input: Record<string, unknown>,
status: "running" | "completed" = "completed",
) {
return {
id: partID,
sessionID,
@@ -197,7 +298,7 @@ function contextTool(partID: string, messageID: string, tool: string, input: Rec
callID: `call_${partID}`,
tool,
state: {
status: "completed",
status,
input,
output: `Completed ${tool}.\n${"detail line\n".repeat(8)}`,
title: input.filePath || input.path || input.pattern || "completed",
@@ -207,13 +308,19 @@ function contextTool(partID: string, messageID: string, tool: string, input: Rec
}
}
async function mockServer(page: Page) {
async function mockServer(
page: Page,
events: { directory: string; payload: Record<string, unknown> }[] = [],
fixtureMessages = messages,
) {
await mockOpenCodeServer(page, {
directory,
project: project(),
provider: provider(),
sessions: [session()],
pageMessages: () => ({ items: messages }),
pageMessages: () => ({ items: fixtureMessages }),
events: () => events.splice(0, 1),
eventRetry: 50,
})
}
@@ -0,0 +1,31 @@
import { expect, test } from "@playwright/test"
import {
assistantMessage,
partUpdated,
setupTimeline,
toolPart,
userMessage,
} from "../performance/timeline-stability/fixture"
test("preserves a collapsed context group through count and status updates", async ({ page }) => {
const ids = ["prt_closed_01_read", "prt_closed_02_glob"]
const inputs = {
read: { filePath: "src/a.ts", offset: 0, limit: 120 },
glob: { path: ".", pattern: "**/*.ts" },
}
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage(
[toolPart(ids[0]!, "read", "running", inputs.read), toolPart(ids[1]!, "glob", "running", inputs.glob)],
{ completed: false },
),
],
})
const group = page.locator(`[data-timeline-part-ids="${ids.join(",")}"]`)
const trigger = group.locator('[data-slot="collapsible-trigger"]')
await expect(trigger).toHaveAttribute("aria-expanded", "false")
await timeline.send(partUpdated(toolPart(ids[0]!, "read", "completed", inputs.read)), 100)
await timeline.send(partUpdated(toolPart(ids[1]!, "glob", "completed", inputs.glob)), 300)
await expect(trigger).toHaveAttribute("aria-expanded", "false")
})
@@ -0,0 +1,52 @@
import { expect, test } from "@playwright/test"
import { assistantMessage, setupTimeline, toolPart, userMessage } from "../performance/timeline-stability/fixture"
test("renders completed write content", async ({ page }) => {
const id = "prt_file_projection_write"
await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([
toolPart(id, "write", "completed", { filePath: "src/write.ts", content: "export const written = true\n" }),
]),
],
settings: { editToolPartsExpanded: true },
})
await expect(page.locator(`[data-timeline-part-id="${id}"] [data-component="write-content"]`)).toBeVisible()
})
test("renders a completed single-file patch", async ({ page }) => {
const id = "prt_file_projection_single_patch"
await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([
toolPart(
id,
"apply_patch",
"completed",
{ files: ["src/a.ts"] },
{
metadata: {
files: [
{
filePath: "src/a.ts",
relativePath: "src/a.ts",
type: "update",
additions: 1,
deletions: 1,
before: "export const value = 1\n",
after: "export const value = 2\n",
},
],
},
},
),
]),
],
settings: { editToolPartsExpanded: true },
})
await expect(page.locator(`[data-timeline-part-id="${id}"] [data-component="apply-patch-file-diff"]`)).toBeVisible()
})
@@ -0,0 +1,94 @@
import { expect, test } from "@playwright/test"
import {
assistantMessage,
partUpdated,
setupTimeline,
toolPart,
userMessage,
} from "../performance/timeline-stability/fixture"
test("updates edit diagnostics without resetting manual collapse state", async ({ page }) => {
const editID = "prt_diagnostics_edit"
const base = editPart(editID, [])
const timeline = await setupTimeline(page, {
messages: [userMessage(), assistantMessage([base])],
settings: { editToolPartsExpanded: true },
})
const trigger = page.locator(`[data-timeline-part-id="${editID}"] [data-slot="collapsible-trigger"]`).first()
await trigger.click()
await expect(trigger).toHaveAttribute("aria-expanded", "false")
await timeline.send(
partUpdated(editPart(editID, [diagnostic("First failure", 2), diagnostic("Second failure", 4)])),
300,
)
await expect(trigger).toHaveAttribute("aria-expanded", "false")
await timeline.send(partUpdated(editPart(editID, [])), 300)
await expect(trigger).toHaveAttribute("aria-expanded", "false")
})
test("preserves nested patch file state through outer collapse and reopen", async ({ page }) => {
const patchID = "prt_nested_patch"
const files = [patchFile("src/a.ts", "update"), patchFile("src/b.ts", "add"), patchFile("src/old.ts", "delete")]
await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([
toolPart(
patchID,
"apply_patch",
"completed",
{ files: files.map((file) => file.filePath) },
{ metadata: { files } },
),
]),
],
settings: { editToolPartsExpanded: true },
})
const wrapper = page.locator(`[data-timeline-part-id="${patchID}"]`)
const outer = wrapper.locator('[data-slot="collapsible-trigger"]').first()
const deleted = wrapper.locator('[data-scope="apply-patch"] [data-type="delete"]')
await deleted.getByRole("button").click()
await expect(deleted.getByRole("button")).toHaveAttribute("aria-expanded", "true")
await outer.click()
await expect(outer).toHaveAttribute("aria-expanded", "false")
await outer.click()
await expect(outer).toHaveAttribute("aria-expanded", "true")
await expect(deleted.getByRole("button")).toHaveAttribute("aria-expanded", "true")
})
function patchFile(filePath: string, type: "add" | "update" | "delete") {
return {
filePath,
relativePath: filePath,
type,
additions: type === "delete" ? 0 : 4,
deletions: type === "add" ? 0 : 3,
before: type === "add" ? undefined : source(false),
after: type === "delete" ? undefined : source(true),
}
}
function editPart(id: string, diagnostics: Record<string, unknown>[]) {
return toolPart(
id,
"edit",
"completed",
{ filePath: "src/edit.ts" },
{
metadata: {
filediff: { file: "src/edit.ts", additions: 1, deletions: 1, before: source(false), after: source(true) },
diagnostics,
},
},
)
}
function diagnostic(message: string, line: number) {
return { message, severity: 1, range: { start: { line, character: 0 }, end: { line, character: 2 } } }
}
function source(changed: boolean) {
return Array.from({ length: 12 }, (_, index) => `export const value${index} = ${changed ? index + 1 : index}\n`).join(
"",
)
}
@@ -0,0 +1,220 @@
import { base64Encode } from "@opencode-ai/core/util/encode"
import { expect, test, type Page } from "@playwright/test"
import {
assistantMessage,
directory,
messageUpdated,
project,
session,
sessionID,
status,
textPart,
title,
userID,
userMessage,
} from "../performance/timeline-stability/fixture"
import { mockOpenCodeServer } from "../utils/mock-server"
import { installSseTransport } from "../utils/sse-transport"
import { expectSessionTitle } from "../utils/waits"
const assistants = Array.from({ length: 14 }, (_, index) =>
assistantMessage([textPart(`prt_history_root_${index}`, `Assistant response ${index}`)], {
id: `msg_${String(index + 1001).padStart(4, "0")}_history_root_assistant`,
parentID: userID,
created: 1700000001000 + index * 1_000,
completed: index < 13,
}),
)
const messages = [userMessage(), ...assistants]
const lastAssistant = assistants.at(-1)!
const lastPartID = assistants.at(-1)!.parts[0]!.id
const userPartID = `prt_${userID}_text`
const completed = {
...lastAssistant.info,
time: { ...lastAssistant.info.time, completed: lastAssistant.info.time.created + 15_000 },
}
const scenarios = [
{ name: "completion", info: completed, idleFirst: false, interrupted: false },
{
name: "interruption",
info: { ...completed, error: { name: "MessageAbortedError", data: { message: "Stopped" } } },
idleFirst: true,
interrupted: true,
},
] as const
test.use({ viewport: { width: 646, height: 1385 } })
for (const scenario of scenarios) {
test(`keeps the latest user turn visible through ${scenario.name}`, async ({ page }) => {
const requests: { before?: string; phase: "start" | "end" }[] = []
const pages: { before?: string; limit: number }[] = []
const roots: { sessionID: string; messageID: string }[] = []
const sequence: string[] = []
const history = Promise.withResolvers<void>()
const transport = await installSseTransport<{ directory: string; payload: Record<string, unknown> }>(page, {
server: `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`,
retry: 20,
})
await mockOpenCodeServer(page, {
directory,
project: project(),
provider: {
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" },
},
sessions: [session()],
sessionStatus: { [sessionID]: { type: "busy" } },
beforeMessagesResponse: (request) => (request.before ? history.promise : Promise.resolve()),
onMessages: (request) => {
requests.push(request)
sequence.push(`messages:${request.phase}:${request.before ?? "latest"}`)
},
onMessage: (request) => {
roots.push(request)
sequence.push(`message:${request.messageID}`)
},
message: (requestedSessionID, messageID) => {
if (requestedSessionID !== sessionID) return
return messages.find((item) => item.info.id === messageID)
},
pageMessages: (_, limit, before) => {
pages.push({ before, limit })
const end = before ? messages.findIndex((message) => message.info.id === before) : messages.length
const start = Math.max(0, end - limit)
return {
items: messages.slice(start, end),
cursor: start > 0 ? messages[start]!.info.id : undefined,
}
},
})
await page.addInitScript(
({ userPartID, lastPartID }) => {
const state = { armed: false, hidden: false, samples: 0, stop: false }
;(window as Window & { __historyRootProbe?: typeof state }).__historyRootProbe = state
const sample = () => {
if (state.armed) {
const virtual = document.querySelector<HTMLElement>("[data-timeline-virtual-content]")
const viewport = virtual?.closest<HTMLElement>(".scroll-view__viewport")
const view = viewport?.getBoundingClientRect()
const visible = (partID: string) => {
const part = viewport?.querySelector<HTMLElement>(`[data-timeline-part-id="${partID}"]`)
const rect = part?.getBoundingClientRect()
return (
!!rect &&
!!view &&
rect.width > 0 &&
rect.height > 0 &&
rect.bottom > view.top &&
rect.top < view.bottom
)
}
if (!virtual || !visible(userPartID) || !visible(lastPartID)) state.hidden = true
state.samples++
}
if (!state.stop) requestAnimationFrame(() => setTimeout(sample, 0))
}
requestAnimationFrame(() => setTimeout(sample, 0))
},
{ userPartID, lastPartID },
)
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
await transport.waitForConnection()
await expectSessionTitle(page, title)
await expect(page.locator(`[data-timeline-part-id="${lastPartID}"]`)).toBeVisible()
await expect(page.locator(`[data-timeline-part-id="${userPartID}"]`)).toBeVisible()
await expect.poll(() => requests.filter((request) => request.phase === "start").length).toBe(2)
expect(requests.filter((request) => request.phase === "end")).toHaveLength(1)
expect(sequence.slice(0, 4)).toEqual([
"messages:start:latest",
"messages:end:latest",
`message:${userID}`,
`messages:start:${messages.at(-2)!.info.id}`,
])
await page.evaluate(() => {
;(
window as Window & {
__historyRootProbe?: { armed: boolean }
}
).__historyRootProbe!.armed = true
})
await waitForProbeSamples(page, 0)
expect(await historyRootHidden(page)).toBe(false)
const beforeHistory = await probeSamples(page)
history.resolve()
await expect(page.locator('[data-timeline-part-id^="prt_history_root_"]')).toHaveCount(14)
await expect(page.getByRole("button", { name: "Stop" })).toBeVisible()
await waitForProbeSamples(page, beforeHistory)
expect(pages[0]).toEqual({ before: undefined, limit: 2 })
expect(roots).toEqual([{ sessionID, messageID: userID }])
const message = messageUpdated(scenario.info)
const idle = status("idle")
for (const event of scenario.idleFirst ? [idle, message] : [message, idle]) {
const beforeEvent = await probeSamples(page)
await transport.send(event)
if (event === idle) await expect(page.getByRole("button", { name: "Stop" })).toHaveCount(0)
if (event === message && scenario.interrupted)
await expect(page.getByText("Interrupted", { exact: true })).toBeVisible()
await waitForProbeSamples(page, beforeEvent)
const current = await timelineState(page)
expect(current, JSON.stringify(current)).toMatchObject({ virtual: true })
expect(current.rows, JSON.stringify(current)).toBeGreaterThan(0)
}
expect(requests[0]).toEqual({ before: undefined, phase: "start", sessionID })
expect(requests[1]).toEqual({ before: undefined, phase: "end", sessionID })
await expect(page.getByRole("button", { name: "Stop" })).toHaveCount(0)
await expect(page.locator('[data-timeline-row="bottom-spacer"]')).toBeVisible()
if (scenario.interrupted) await expect(page.getByText("Interrupted", { exact: true })).toBeVisible()
expect(
await page.evaluate(() => {
const state = (window as Window & { __historyRootProbe?: { hidden: boolean; stop: boolean } })
.__historyRootProbe!
state.stop = true
return state.hidden
}),
).toBe(false)
})
}
function timelineState(page: Page) {
return page.evaluate(() => ({
virtual: !!document.querySelector("[data-timeline-virtual-content]"),
rows: document.querySelectorAll("[data-timeline-key]").length,
}))
}
function probeSamples(page: Page) {
return page.evaluate(
() => (window as Window & { __historyRootProbe?: { samples: number } }).__historyRootProbe!.samples,
)
}
async function waitForProbeSamples(page: Page, after: number) {
await page.waitForFunction(
(after) =>
(window as Window & { __historyRootProbe?: { samples: number } }).__historyRootProbe!.samples >= after + 3,
after,
)
}
function historyRootHidden(page: Page) {
return page.evaluate(
() => (window as Window & { __historyRootProbe?: { hidden: boolean } }).__historyRootProbe!.hidden,
)
}
@@ -0,0 +1,94 @@
import { expect, test } from "@playwright/test"
import {
assistantMessage,
completedAssistantInfo,
messageUpdated,
partUpdated,
reasoningPart,
setupTimeline,
shell,
status,
textPart,
userMessage,
} from "../performance/timeline-stability/fixture"
for (const expanded of [false, true]) {
test(`preserves shell user intent from a ${expanded ? "expanded" : "collapsed"} default`, async ({ page }) => {
const id = `prt_shell_default_${expanded}`
const timeline = await setupTimeline(page, {
messages: [userMessage(), assistantMessage([shell(id, "completed", lines(3))])],
settings: { shellToolPartsExpanded: expanded },
})
const trigger = page.locator(`[data-timeline-part-id="${id}"] [data-slot="collapsible-trigger"]`)
await expect(trigger).toHaveAttribute("aria-expanded", String(expanded))
await trigger.click()
await expect(trigger).toHaveAttribute("aria-expanded", String(!expanded))
await timeline.send(partUpdated(shell(id, "completed", lines(6))), 180)
await timeline.send(partUpdated(textPart(`prt_sibling_${expanded}`, "Sibling content")), 180)
await timeline.send(status("busy"), 100)
await timeline.send(status("idle"), 250)
await expect(trigger).toHaveAttribute("aria-expanded", String(!expanded))
})
}
test("transitions thinking and hidden reasoning through busy to idle", async ({ page }) => {
const reasoningID = "prt_reasoning_hidden"
const assistant = assistantMessage([reasoningPart(reasoningID, "## Inspecting stability")], { completed: false })
const timeline = await setupTimeline(page, {
messages: [userMessage(), assistant],
settings: { showReasoningSummaries: false },
cpuRate: 4,
})
await timeline.send(status("busy"), 150)
await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible()
await expect(page.getByText("Inspecting stability", { exact: true })).toBeVisible()
await expect(page.locator(`[data-timeline-part-id="${reasoningID}"]`)).toHaveCount(0)
await timeline.send(partUpdated(shell("prt_reasoning_shell", "running")), 160)
await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible()
await timeline.send(partUpdated(shell("prt_reasoning_shell", "completed", "done")), 180)
await timeline.send(messageUpdated(completedAssistantInfo(assistant.info)), 100)
await timeline.send(status("idle"), 300)
await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0)
await expect(page.locator(`[data-timeline-part-id="${reasoningID}"]`)).toHaveCount(0)
})
test("moves busy through retry and recovery to final idle content", async ({ page }) => {
const assistant = assistantMessage([], { completed: false })
const timeline = await setupTimeline(page, {
messages: [
userMessage(undefined, {
summary: {
diffs: [
{
file: "src/retry.ts",
additions: 1,
deletions: 1,
patch: "@@ -1 +1 @@\n-export const retry = false\n+export const retry = true",
},
],
},
}),
assistant,
],
})
await timeline.send(status("busy"), 140)
await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible()
await expect(page.locator('[data-timeline-row="DiffSummary"]')).toHaveCount(0)
await timeline.send(status("retry"), 180)
await expect(page.locator('[data-timeline-row="Retry"]')).toBeVisible()
await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0)
await timeline.send(status("busy", 2), 180)
await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible()
await timeline.send(partUpdated(textPart("prt_recovered", "Recovered response")), 140)
await timeline.send(messageUpdated(completedAssistantInfo(assistant.info)), 100)
await timeline.send(status("idle"), 350)
await expect(page.locator('[data-timeline-row="Retry"]')).toHaveCount(0)
await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0)
await expect(page.locator('[data-timeline-row="DiffSummary"]')).toBeVisible()
})
function lines(count: number) {
return Array.from({ length: count }, (_, index) => `line ${index + 1}`).join("\n")
}
@@ -0,0 +1,25 @@
import { expect, test } from "@playwright/test"
import { assistantMessage, setupTimeline, toolPart, userMessage } from "../performance/timeline-stability/fixture"
for (const profile of [
{ locale: "de", label: "Erkundet" },
{ locale: "ar", label: "تم الاستكشاف" },
] as const) {
test(`projects translated context status in ${profile.locale}`, async ({ page }) => {
const ids = [`prt_locale_${profile.locale}_01_read`, `prt_locale_${profile.locale}_02_glob`]
await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([
toolPart(ids[0]!, "read", "completed", { filePath: "src/a.ts" }),
toolPart(ids[1]!, "glob", "completed", { path: ".", pattern: "**/*.ts" }),
]),
],
locale: profile.locale,
})
const group = page.locator(`[data-timeline-part-ids="${ids.join(",")}"]`)
await expect(group.locator('[data-component="tool-status-title"]')).toHaveAttribute("aria-label", profile.label)
await expect(page.locator("html")).toHaveAttribute("lang", profile.locale)
})
}
@@ -0,0 +1,287 @@
import { expect, test } from "@playwright/test"
import {
assistantMessage,
setupTimeline,
status,
toolPart,
userMessage,
userText,
type PartSeed,
} from "../performance/timeline-stability/fixture"
test.describe("session timeline projection", () => {
test("renders every admitted tool family and hides timeline-only exclusions", async ({ page }) => {
const parts = [
toolPart("prt_01_read", "read", "completed", { filePath: "src/a.ts" }),
toolPart("prt_02_glob", "glob", "completed", { path: ".", pattern: "**/*.ts" }),
toolPart("prt_03_grep", "grep", "completed", { path: ".", pattern: "value" }),
toolPart("prt_04_list", "list", "completed", { path: "src" }),
toolPart("prt_webfetch", "webfetch", "completed", { url: "https://example.com" }),
toolPart(
"prt_websearch",
"websearch",
"completed",
{ query: "timeline stability" },
{ output: "https://example.com/result" },
),
toolPart("prt_task", "task", "completed", { description: "Inspect timeline", subagent_type: "explore" }),
toolPart(
"prt_bash",
"bash",
"completed",
{ command: "printf stable" },
{ output: "stable", title: "printf stable" },
),
editPart("prt_edit"),
toolPart("prt_write", "write", "completed", { filePath: "src/new.ts", content: "export const stable = true\n" }),
patchPart("prt_patch"),
toolPart("prt_todo", "todowrite", "completed", { todos: [{ content: "Hidden", status: "pending" }] }),
toolPart(
"prt_question",
"question",
"completed",
{ questions: [{ question: "Keep stable?", header: "Stability", options: [] }] },
{ metadata: { answers: [["Yes"]] } },
),
toolPart("prt_skill", "skill", "completed", { name: "stability" }),
toolPart("prt_custom", "custom_mcp_tool", "completed", { target: "timeline", count: 2 }),
]
await setupTimeline(page, { messages: [userMessage(), assistantMessage(parts)] })
await expect(
page.locator('[data-timeline-part-ids="prt_01_read,prt_02_glob,prt_03_grep,prt_04_list"]'),
).toBeVisible()
for (const id of [
"prt_webfetch",
"prt_websearch",
"prt_task",
"prt_bash",
"prt_edit",
"prt_write",
"prt_patch",
"prt_question",
"prt_skill",
"prt_custom",
]) {
await expect(page.locator(`[data-timeline-part-id="${id}"]`).first(), id).toBeVisible()
}
await expect(page.locator('[data-timeline-part-id="prt_todo"]')).toHaveCount(0)
})
test("projects gaps, dividers, assistant parts, and errors together", async ({ page }) => {
const firstUser = userMessage(
[
userText("The user made the following comment regarding lines 4 through 8 of src/a.ts: Keep this stable", {
id: "prt_comment",
synthetic: true,
metadata: {
opencodeComment: {
path: "src/a.ts",
selection: { startLine: 4, startChar: 0, endLine: 8, endChar: 0 },
comment: "Keep this stable",
},
},
}),
userText("Continue after the comment", { id: "prt_visible_user" }),
],
{ summary: { diffs: Array.from({ length: 11 }, (_, index) => summaryDiff(index)) } },
)
const aborted = assistantMessage(
[
{ id: "prt_before_abort", type: "text", text: "Before interruption" },
{ id: "prt_compaction", type: "compaction", auto: true },
],
{
id: "msg_1001_assistant_aborted",
error: { name: "MessageAbortedError", data: { message: "Stopped" } },
},
)
const failed = assistantMessage([{ id: "prt_after_abort", type: "text", text: "After interruption" }], {
id: "msg_1002_assistant_failed",
error: {
name: "APIError",
data: {
message: JSON.stringify({ error: { type: "provider_error", message: "Visible provider failure" } }),
isRetryable: false,
},
},
created: 1700000003000,
})
const nextUser = userMessage([userText("Second turn", { id: "prt_second_user" })], {
id: "msg_2000_second_user",
created: 1700000005000,
})
const nextAssistant = assistantMessage([{ id: "prt_second_text", type: "text", text: "Second response" }], {
id: "msg_2001_second_assistant",
parentID: "msg_2000_second_user",
created: 1700000006000,
})
const timeline = await setupTimeline(page, { messages: [firstUser, aborted, failed, nextUser, nextAssistant] })
await timeline.send(status("idle"), 100)
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
await scroller.evaluate((element) => (element.scrollTop = 0))
await expect(page.locator('[data-timeline-row="TurnDivider"]')).toHaveCount(1)
await expect(page.getByText("Session compacted", { exact: true })).toBeVisible()
await expect(page.getByText("Visible provider failure")).toBeVisible()
await scroller.evaluate((element) => (element.scrollTop = element.scrollHeight))
await expect(page.locator('[data-timeline-row="TurnGap"]')).toBeVisible()
})
test("renders comment strips and historical diff summary overflow", async ({ page }) => {
const user = userMessage(
[
userText("The user made the following comment regarding lines 4 through 8 of src/a.ts: Keep this stable", {
id: "prt_comment_only",
synthetic: true,
metadata: {
opencodeComment: {
path: "src/a.ts",
selection: { startLine: 4, startChar: 0, endLine: 8, endChar: 0 },
comment: "Keep this stable",
},
},
}),
userText("Continue after the comment", { id: "prt_comment_visible" }),
],
{ summary: { diffs: Array.from({ length: 11 }, (_, index) => summaryDiff(index)) } },
)
const nextUser = userMessage(undefined, { id: "msg_2000_diff_next_user", created: 1700000010000 })
const nextAssistant = assistantMessage([], {
id: "msg_2001_diff_next_assistant",
parentID: "msg_2000_diff_next_user",
created: 1700000011000,
})
await setupTimeline(page, {
messages: [user, assistantMessage(), nextUser, nextAssistant],
settings: { newLayoutDesigns: false },
})
const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") })
await scroller.evaluate((element) => (element.scrollTop = 0))
await expect(page.locator('[data-timeline-row="CommentStrip"]')).toBeVisible()
await expect(page.getByText("Keep this stable", { exact: true })).toBeVisible()
await expect(page.locator('[data-timeline-row="DiffSummary"]')).toBeVisible()
await expect(page.getByText(/show all/i)).toBeVisible()
})
test("renders interruption independently when the turn is not compacted", async ({ page }) => {
const user = userMessage()
const before = assistantMessage([{ id: "prt_before", type: "text", text: "Before" }], {
id: "msg_1001_before",
error: { name: "MessageAbortedError", data: { message: "Stopped" } },
})
const after = assistantMessage([{ id: "prt_after", type: "text", text: "After" }], {
id: "msg_1002_after",
created: 1700000003000,
})
await setupTimeline(page, { messages: [user, before, after] })
await expect(page.getByText("Interrupted", { exact: true })).toBeVisible()
const rows = await page
.locator('[data-timeline-row="AssistantPart"], [data-timeline-row="TurnDivider"]')
.evaluateAll((elements) => elements.map((element) => element.getAttribute("data-timeline-row")))
expect(rows).toEqual(["AssistantPart", "TurnDivider", "AssistantPart"])
})
test("renders user image, file attachment, file reference, and agent reference", async ({ page }) => {
const text = "Use @explore with @src/a.ts and inspect the attachments"
const parts: PartSeed<"user">[] = [
userText(text, { id: "prt_user_rich" }),
{
id: "prt_user_image",
type: "file",
mime: "image/png",
filename: "pixel.png",
url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
},
{
id: "prt_user_attachment",
type: "file",
mime: "application/json",
filename: "tsconfig.json",
url: "data:application/json;base64,e30=",
},
{
id: "prt_user_reference",
type: "file",
mime: "text/plain",
filename: "a.ts",
url: "src/a.ts",
source: { type: "file", path: "src/a.ts", text: { value: "@src/a.ts", start: 18, end: 27 } },
},
{
id: "prt_user_agent",
type: "agent",
name: "explore",
source: { value: "@explore", start: 4, end: 12 },
},
]
await setupTimeline(page, { messages: [userMessage(parts), assistantMessage()] })
await expect(page.getByAltText("pixel.png")).toBeVisible()
await expect(page.getByText("tsconfig.json")).toBeVisible()
await expect(page.getByText("@src/a.ts", { exact: true })).toBeVisible()
await expect(page.getByText("@explore", { exact: true })).toBeVisible()
})
})
function editPart(id: string) {
return toolPart(
id,
"edit",
"completed",
{ filePath: "src/a.ts" },
{
metadata: {
filediff: {
file: "src/a.ts",
additions: 1,
deletions: 1,
before: "export const value = 1\n",
after: "export const value = 2\n",
},
},
},
)
}
function patchPart(id: string) {
return toolPart(
id,
"apply_patch",
"completed",
{ files: ["src/a.ts", "src/b.ts"] },
{
metadata: {
files: [
patchFile("src/a.ts", "update"),
patchFile("src/b.ts", "add"),
patchFile("src/old.ts", "delete"),
{ ...patchFile("src/moved.ts", "move"), move: "src/new-place.ts" },
],
},
},
)
}
function patchFile(filePath: string, type: "add" | "update" | "delete" | "move") {
return {
filePath,
relativePath: filePath,
type,
additions: type === "delete" ? 0 : 1,
deletions: type === "add" ? 0 : 1,
before: type === "add" ? undefined : "export const before = true\n",
after: type === "delete" ? undefined : "export const after = true\n",
}
}
function summaryDiff(index: number) {
return {
file: `src/diff-${index}.ts`,
additions: 1,
deletions: 1,
patch: `@@ -1 +1 @@\n-export const value = ${index}\n+export const value = ${index + 1}`,
}
}
@@ -0,0 +1,93 @@
import { expect, test } from "@playwright/test"
import {
assistantMessage,
reasoningPart,
setupTimeline,
status,
textPart,
toolPart,
userMessage,
} from "../performance/timeline-stability/fixture"
const profiles = [
{ name: "summaries off no reasoning", summaries: false, reasoning: "", other: false, thinking: true, body: false },
{
name: "summaries off reasoning heading",
summaries: false,
reasoning: "## Inspecting stability",
other: false,
thinking: true,
body: false,
},
{
name: "summaries off with visible tool",
summaries: false,
reasoning: "## Inspecting stability",
other: true,
thinking: true,
body: false,
},
{ name: "summaries on no content", summaries: true, reasoning: "", other: false, thinking: true, body: false },
{
name: "summaries on blank reasoning",
summaries: true,
reasoning: " ",
other: false,
thinking: true,
body: false,
},
{
name: "summaries on visible reasoning",
summaries: true,
reasoning: "## Inspecting stability",
other: false,
thinking: false,
body: true,
},
{
name: "summaries on visible tool no reasoning",
summaries: true,
reasoning: "",
other: true,
thinking: false,
body: false,
},
] as const
for (const profile of profiles) {
test(`projects busy reasoning profile ${profile.name}`, async ({ page }) => {
const reasoningID = `prt_reasoning_matrix_${profiles.indexOf(profile)}`
const parts = [
...(profile.reasoning ? [reasoningPart(reasoningID, profile.reasoning)] : []),
...(profile.other
? [toolPart(`prt_reasoning_tool_${profiles.indexOf(profile)}`, "skill", "running", { name: "inspect" })]
: []),
]
const timeline = await setupTimeline(page, {
messages: [userMessage(), assistantMessage(parts, { completed: false })],
settings: { showReasoningSummaries: profile.summaries },
})
await timeline.send(status("busy"), 150)
await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(profile.thinking ? 1 : 0)
await expect(page.locator(`[data-timeline-part-id="${reasoningID}"]`)).toHaveCount(profile.body ? 1 : 0)
if (!profile.summaries && profile.reasoning.trim()) {
await expect(page.getByText("Inspecting stability", { exact: true })).toBeVisible()
}
})
}
test("does not infer reasoning visibility from provider identity", async ({ page }) => {
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([textPart("prt_provider_text", "No reasoning payload")], { completed: false }),
],
settings: { showReasoningSummaries: true },
})
await timeline.send(status("busy"), 150)
await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0)
await expect(page.locator('[data-timeline-part-id*="reasoning"]')).toHaveCount(0)
await expect(page.locator('[data-timeline-part-id="prt_provider_text"]')).toBeVisible()
})
@@ -0,0 +1,43 @@
import { expect, test } from "@playwright/test"
import {
assistantMessage,
completedAssistantInfo,
messageUpdated,
partUpdated,
setupTimeline,
shell,
status,
textPart,
toolPart,
userMessage,
} from "../performance/timeline-stability/fixture"
test("groups singleton and separated context operations at correct boundaries", async ({ page }) => {
const parts = [
toolPart("prt_boundary_01_read", "read", "completed", { filePath: "src/a.ts" }),
textPart("prt_boundary_02_text", "Boundary text"),
toolPart("prt_boundary_03_glob", "glob", "completed", { path: ".", pattern: "**/*.ts" }),
toolPart("prt_boundary_04_grep", "grep", "completed", { path: ".", pattern: "stable" }),
shell("prt_boundary_05_shell", "completed", "done"),
toolPart("prt_boundary_06_list", "list", "completed", { path: "src" }),
]
await setupTimeline(page, { messages: [userMessage(), assistantMessage(parts)] })
await expect(page.locator('[data-timeline-part-ids="prt_boundary_01_read"]')).toBeVisible()
await expect(page.locator('[data-timeline-part-ids="prt_boundary_03_glob,prt_boundary_04_grep"]')).toBeVisible()
await expect(page.locator('[data-timeline-part-ids="prt_boundary_06_list"]')).toBeVisible()
await expect(page.locator('[data-timeline-row="AssistantPart"]')).toHaveCount(5)
})
test("reducer-hardening: converges when idle arrives before final part and message completion", async ({ page }) => {
const textID = "prt_event_order_text"
const assistant = assistantMessage([textPart(textID, "Partial")], { completed: false })
const timeline = await setupTimeline(page, { messages: [userMessage(), assistant] })
await timeline.send(status("busy"), 100)
await timeline.send(status("idle"), 100)
await timeline.send(partUpdated(textPart(textID, "Final after early idle")), 120)
await timeline.send(messageUpdated(completedAssistantInfo(assistant.info)), 250)
await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0)
await expect(page.locator(`[data-timeline-part-id="${textID}"]`)).toContainText("Final after early idle")
})
@@ -0,0 +1,228 @@
import { expect, test, type Locator, type Page } from "@playwright/test"
import {
assistantMessage,
setupTimeline,
shell,
textPart,
toolPart,
userMessage,
} from "../performance/timeline-stability/fixture"
for (const deviceScaleFactor of [1.25, 1.5]) {
test(`keeps the shell outline inside a fractionally short virtual row at ${deviceScaleFactor}x`, async ({ page }) => {
const shellID = "prt_shell_outline"
const timeline = await setupTimeline(page, {
messages: [userMessage(), assistantMessage([shell(shellID, "completed", "shell output")])],
settings: { newLayoutDesigns: true, shellToolPartsExpanded: true },
reducedMotion: true,
deviceScaleFactor,
})
const part = page.locator(`[data-timeline-part-id="${shellID}"]`)
const output = part.locator('[data-component="bash-output"]')
const row = page.locator("[data-timeline-key]", { has: part })
await expect(output).toBeVisible()
await timeline.settle()
const geometry = await row.evaluate((element) => {
const output = element.querySelector<HTMLElement>('[data-component="bash-output"]')
if (!output) throw new Error("Shell output is unavailable")
const rowRect = element.getBoundingClientRect()
const outputRect = output.getBoundingClientRect()
// Match a rounded-down measurement at a fractional device-pixel phase.
element.style.height = `${outputRect.bottom - rowRect.top - 0.49}px`
element.style.transform = "translateY(0.25px)"
output.style.setProperty("--v2-border-border-base", "rgb(255, 0, 255)")
output.style.setProperty("background", "rgb(0, 0, 0)", "important")
const style = getComputedStyle(output)
return {
outputWidth: outputRect.width,
outputHeight: outputRect.height,
borderColor: style.borderTopColor,
boxShadow: style.boxShadow,
clipMargin: getComputedStyle(element).overflowClipMargin,
}
})
await timeline.settle()
const clipped = await row.evaluate((element) => {
const output = element.querySelector<HTMLElement>('[data-component="bash-output"]')!
return output.getBoundingClientRect().bottom - element.getBoundingClientRect().bottom
})
expect(clipped).toBeCloseTo(0.49, 1)
expect(await page.evaluate(() => devicePixelRatio)).toBe(deviceScaleFactor)
const edges = await captureCardEdges(page, output)
expect(edges.box.width).toBeCloseTo(geometry.outputWidth, 2)
expect(edges.box.height).toBeCloseTo(geometry.outputHeight, 2)
expect(geometry.borderColor).toBe("rgb(255, 0, 255)")
expect(geometry.boxShadow).toBe("none")
expect(geometry.clipMargin).toBe("0.5px")
expect(edges.magenta.top).toBeGreaterThan(0.75)
expect(edges.magenta.bottom).toBeGreaterThan(0.75)
expect(edges.magenta.vertical).toBeGreaterThanOrEqual(2)
})
}
test("keeps the patch card inside a fractionally short virtual row", async ({ page }) => {
const patchID = "prt_patch_outline"
const file = {
filePath: "src/outline.ts",
relativePath: "src/outline.ts",
type: "update",
additions: 1,
deletions: 1,
before: "const outline = false\n",
after: "const outline = true\n",
}
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([
toolPart(patchID, "apply_patch", "completed", { files: [file.filePath] }, { metadata: { files: [file] } }),
]),
],
settings: { editToolPartsExpanded: true, newLayoutDesigns: true },
reducedMotion: true,
})
const part = page.locator(`[data-timeline-part-id="${patchID}"]`)
const card = part.locator('[data-component="accordion"][data-scope="apply-patch"]')
const row = page.locator("[data-timeline-key]", { has: part })
await expect(card).toBeVisible()
await timeline.settle()
const geometry = await row.evaluate((element) => {
const card = element.querySelector<HTMLElement>('[data-component="accordion"][data-scope="apply-patch"]')
if (!card) throw new Error("Patch card is unavailable")
const rowRect = element.getBoundingClientRect()
const cardRect = card.getBoundingClientRect()
element.style.height = `${cardRect.bottom - rowRect.top - 0.49}px`
const clipMargin = getComputedStyle(element).overflowClipMargin
const bottom = element.getBoundingClientRect().bottom
return {
overflow: card.getBoundingClientRect().bottom - bottom,
paintOverflow: card.getBoundingClientRect().bottom - bottom - Number.parseFloat(clipMargin),
clipMargin,
cardWidth: cardRect.width,
cardHeight: cardRect.height,
}
})
await timeline.settle()
expect(geometry.overflow).toBeCloseTo(0.49, 1)
expect(geometry.paintOverflow).toBeLessThanOrEqual(0)
const edges = await captureCardEdges(page, card)
expect(edges.box.width).toBeCloseTo(geometry.cardWidth, 2)
expect(edges.box.height).toBeCloseTo(geometry.cardHeight, 2)
expect(edges.luminance.top).toBeLessThan(245)
expect(edges.luminance.bottom).toBeLessThan(245)
expect(Math.abs(edges.luminance.bottom - edges.luminance.top)).toBeLessThan(10)
expect(geometry.clipMargin).toBe("0.5px")
})
test("allows paint rounding for every framed row but not fixed turn gaps", async ({ page }) => {
const secondUserID = "msg_outline_second_user"
await setupTimeline(page, {
messages: [
userMessage(undefined, {
summary: {
diffs: [
{
file: "src/summary.ts",
additions: 1,
deletions: 1,
patch: "@@ -1 +1 @@\n-export const value = 1\n+export const value = 2",
},
],
},
}),
assistantMessage([textPart("prt_outline_text", "Assistant text")]),
userMessage(undefined, { id: secondUserID, created: 1700000010000 }),
assistantMessage([], {
id: "msg_outline_second_assistant",
parentID: secondUserID,
created: 1700000011000,
}),
],
})
await expect(page.locator('[data-timeline-row="DiffSummary"]')).toBeVisible()
await expect(page.locator('[data-timeline-row="TurnGap"]')).toBeVisible()
const rows = await page.locator("[data-timeline-key]").evaluateAll((elements) =>
elements.map((element) => ({
tag: element.querySelector<HTMLElement>("[data-timeline-row]")?.dataset.timelineRow,
clipMargin: getComputedStyle(element).overflowClipMargin,
})),
)
expect(rows.filter((row) => row.tag !== "TurnGap").every((row) => row.clipMargin === "0.5px")).toBe(true)
expect(rows.filter((row) => row.tag === "TurnGap")).toEqual([{ tag: "TurnGap", clipMargin: "0px" }])
})
async function captureCardEdges(page: Page, card: Locator) {
const box = await card.boundingBox()
if (!box) throw new Error("Tool card bounds are unavailable")
const viewport = page.viewportSize()
if (!viewport) throw new Error("Viewport bounds are unavailable")
const screenshot = await page.screenshot()
return page.evaluate(
async ({ source, box, viewport }) => {
const image = new Image()
image.src = source
await image.decode()
const canvas = document.createElement("canvas")
canvas.width = image.naturalWidth
canvas.height = image.naturalHeight
const context = canvas.getContext("2d")
if (!context) throw new Error("2D canvas is unavailable")
context.drawImage(image, 0, 0)
const scale = {
x: image.naturalWidth / viewport.width,
y: image.naturalHeight / viewport.height,
}
const rows = (candidates: number[]) => {
const left = Math.floor((box.x + 8) * scale.x)
const width = Math.floor((box.width - 16) * scale.x)
return candidates.map((row) => {
const pixels = context.getImageData(left, row, width, 1).data
const indexes = Array.from({ length: width }, (_, index) => index * 4)
return {
luminance:
indexes
.map((index) => (pixels[index]! + pixels[index + 1]! + pixels[index + 2]!) / 3)
.reduce((sum, value) => sum + value, 0) / width,
magenta:
indexes.filter((index) => pixels[index]! > 200 && pixels[index + 1]! < 180 && pixels[index + 2]! > 200)
.length / width,
}
})
}
const pixels = context.getImageData(0, 0, image.naturalWidth, image.naturalHeight).data
const columns = new Uint32Array(image.naturalWidth)
for (let index = 0; index < pixels.length; index += 4) {
if (pixels[index]! <= 200 || pixels[index + 1]! >= 180 || pixels[index + 2]! <= 200) continue
columns[(index / 4) % image.naturalWidth] = columns[(index / 4) % image.naturalWidth]! + 1
}
const top = box.y * scale.y
const bottom = (box.y + box.height) * scale.y
const topRows = rows([Math.floor(top) - 1, Math.floor(top), Math.ceil(top)])
const bottomRows = rows([Math.floor(bottom) - 2, Math.floor(bottom) - 1, Math.ceil(bottom) - 1])
return {
box,
luminance: {
top: Math.min(...topRows.map((row) => row.luminance)),
bottom: rows([Math.ceil(bottom) - 1])[0]!.luminance,
},
magenta: {
top: Math.max(...topRows.map((row) => row.magenta)),
bottom: Math.max(...bottomRows.map((row) => row.magenta)),
vertical: Array.from(columns).filter((count) => count > box.height * scale.y * 0.75).length,
},
}
},
{
source: `data:image/png;base64,${screenshot.toString("base64")}`,
viewport,
box,
},
)
}
@@ -0,0 +1,99 @@
import { expect, test } from "@playwright/test"
import {
assistantMessage,
partUpdated,
setupTimeline,
toolPart,
userMessage,
} from "../performance/timeline-stability/fixture"
test("renders every tool error outcome without leaking hidden tools", async ({ page }) => {
const ordinary = ["bash", "edit", "write", "apply_patch", "webfetch", "websearch", "task", "skill", "mcp_probe"]
const parts = ordinary.map((tool, index) =>
toolPart(`prt_error_${index}`, tool, "error", errorInput(tool), { error: `${tool} failed visibly` }),
)
parts.push(
toolPart("prt_question_dismissed", "question", "error", questionInput(), {
error: "The user dismissed this question",
}),
toolPart("prt_question_error", "question", "error", questionInput(), { error: "Question transport failed" }),
toolPart("prt_todo_error", "todowrite", "error", { todos: [] }, { error: "Hidden todo failure" }),
)
await setupTimeline(page, { messages: [userMessage(), assistantMessage(parts)] })
await expect(page.locator('[data-kind="tool-error-card"]')).toHaveCount(ordinary.length + 1)
await expect(page.getByText(/dismissed/i)).toBeVisible()
await expect(page.locator('[data-timeline-part-id="prt_todo_error"]')).toHaveCount(0)
for (let index = 0; index < ordinary.length; index++) {
await expect(page.locator(`[data-timeline-part-id="prt_error_${index}"]`)).toBeVisible()
}
})
test("transitions shell and question through running error outcomes", async ({ page }) => {
const shellID = "prt_transition_error_shell"
const questionID = "prt_transition_error_question"
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage(
[
toolPart(shellID, "bash", "pending", { command: "exit 1" }),
toolPart(questionID, "question", "pending", questionInput()),
],
{ completed: false },
),
],
})
await timeline.waitForPart(shellID)
await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toHaveCount(0)
await timeline.send(partUpdated(toolPart(shellID, "bash", "running", { command: "exit 1" })), 120)
await timeline.send(partUpdated(toolPart(questionID, "question", "running", questionInput())), 180)
await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toHaveCount(0)
await timeline.send(
partUpdated(toolPart(shellID, "bash", "error", { command: "exit 1" }, { error: "Command exited 1" })),
180,
)
await timeline.send(
partUpdated(
toolPart(questionID, "question", "error", questionInput(), { error: "The user dismissed this question" }),
),
250,
)
await expect(page.locator(`[data-timeline-part-id="${shellID}"] [data-kind="tool-error-card"]`)).toBeVisible()
await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toContainText(/dismissed/i)
})
test("labels all web search provider variants", async ({ page }) => {
const parts = [
toolPart(
"prt_search_parallel",
"websearch",
"completed",
{ query: "parallel" },
{ metadata: { provider: "parallel" } },
),
toolPart("prt_search_exa", "websearch", "completed", { query: "exa" }, { metadata: { provider: "exa" } }),
toolPart("prt_search_generic", "websearch", "completed", { query: "generic" }),
]
await setupTimeline(page, { messages: [userMessage(), assistantMessage(parts)] })
await expect(page.getByRole("button", { name: /Parallel Web Search/ })).toBeVisible()
await expect(page.getByRole("button", { name: /Exa Web Search/ })).toBeVisible()
await expect(page.getByRole("button", { name: /^Web Search/ })).toBeVisible()
})
function questionInput() {
return { questions: [{ header: "Stability", question: "Keep it stable?", options: [] }] }
}
function errorInput(tool: string) {
if (tool === "bash") return { command: "exit 1" }
if (["edit", "write"].includes(tool)) return { filePath: "src/error.ts", content: "" }
if (tool === "apply_patch") return { files: ["src/error.ts"] }
if (tool === "webfetch") return { url: "https://example.com" }
if (tool === "websearch") return { query: "failure" }
if (tool === "task") return { description: "Fail task", subagent_type: "explore" }
if (tool === "skill") return { name: "failure" }
return { target: "failure" }
}
@@ -0,0 +1,77 @@
import { expect, test } from "@playwright/test"
import {
assistantMessage,
partUpdated,
setupTimeline,
toolPart,
userMessage,
} from "../performance/timeline-stability/fixture"
test("updates expanded web search links without resetting expansion", async ({ page }) => {
const searchID = "prt_websearch_mutation"
const input = { query: "timeline stability" }
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([toolPart(searchID, "websearch", "completed", input, { output: "https://example.com/one" })]),
],
})
const wrapper = page.locator(`[data-timeline-part-id="${searchID}"]`)
const trigger = wrapper.locator('[data-slot="collapsible-trigger"]')
await trigger.click()
await expect(trigger).toHaveAttribute("aria-expanded", "true")
await timeline.send(
partUpdated(
toolPart(searchID, "websearch", "completed", input, {
output: "https://example.com/one\nhttps://example.com/two",
}),
),
300,
)
await expect(trigger).toHaveAttribute("aria-expanded", "true")
await expect(wrapper.locator('a[href="https://example.com/two"]')).toBeVisible()
})
test("preserves an expanded tool error card across duplicate delivery", async ({ page }) => {
const toolID = "prt_duplicate_error"
const failed = toolPart(toolID, "bash", "error", { command: "exit 1" }, { error: "Command failed visibly" })
const timeline = await setupTimeline(page, { messages: [userMessage(), assistantMessage([failed])] })
const wrapper = page.locator(`[data-timeline-part-id="${toolID}"]`)
const trigger = wrapper.locator('[data-slot="collapsible-trigger"]')
await trigger.click()
await expect(trigger).toHaveAttribute("aria-expanded", "true")
await timeline.send(partUpdated(failed), 150)
await timeline.send(partUpdated(failed), 250)
await expect(trigger).toHaveAttribute("aria-expanded", "true")
await expect(wrapper).toContainText("Command failed visibly")
})
test("renders multiple question answers and preserves open state on answer updates", async ({ page }) => {
const questionID = "prt_multi_question"
const input = {
questions: [
{ header: "First", question: "First choice?", options: [] },
{ header: "Second", question: "Second choice?", options: [], multiple: true },
],
}
const timeline = await setupTimeline(page, {
messages: [
userMessage(),
assistantMessage([
toolPart(questionID, "question", "completed", input, { metadata: { answers: [["A"], ["B", "C"]] } }),
]),
],
})
const wrapper = page.locator(`[data-timeline-part-id="${questionID}"]`)
const trigger = wrapper.locator('[data-slot="collapsible-trigger"]')
await expect(trigger).toHaveAttribute("aria-expanded", "true")
await timeline.send(
partUpdated(
toolPart(questionID, "question", "completed", input, { metadata: { answers: [["Updated"], ["B", "C"]] } }),
),
300,
)
await expect(trigger).toHaveAttribute("aria-expanded", "true")
await expect(wrapper).toContainText("Updated")
await expect(wrapper).toContainText("B, C")
})
@@ -0,0 +1,116 @@
import { expect, test } from "@playwright/test"
import {
assistantMessage,
partUpdated,
setupTimeline,
status,
textPart,
userMessage,
} from "../performance/timeline-stability/fixture"
test("keeps one connection open while delivering multiple events", async ({ page }) => {
const timeline = await setupTimeline(page)
const first = await timeline.transport.send(partUpdated(textPart("prt_transport_first", "first event")))
const second = await timeline.transport.send(partUpdated(textPart("prt_transport_second", "second event")))
await timeline.waitForPart("prt_transport_first")
await timeline.waitForPart("prt_transport_second")
expect(first.connectionID).toBe(second.connectionID)
expect(await timeline.transport.connections()).toHaveLength(1)
expect(await timeline.transport.acknowledgements()).toHaveLength(2)
})
test("delivers a burst from one stream chunk", async ({ page }) => {
const timeline = await setupTimeline(page)
const acknowledgements = await timeline.transport.burst([
partUpdated(textPart("prt_transport_burst_a", "burst a")),
partUpdated(textPart("prt_transport_burst_b", "burst b")),
])
await timeline.waitForPart("prt_transport_burst_a")
await timeline.waitForPart("prt_transport_burst_b")
expect(acknowledgements.map((item) => item.chunkCount)).toEqual([1, 1])
expect(new Set(acknowledgements.map((item) => item.deliveryID)).size).toBe(2)
})
test("parses split JSON and a split multibyte code point", async ({ page }) => {
const timeline = await setupTimeline(page)
const payload = partUpdated(textPart("prt_transport_split", "split snowman \u2603\u2603\u2603"))
const encoded = new TextEncoder().encode(`data: ${JSON.stringify(payload)}\n\n`)
const snowman = new TextEncoder().encode("\u2603")[0]!
const multibyte = encoded.indexOf(snowman)
const acknowledgement = await timeline.transport.split(payload, [9, multibyte + 1, multibyte + 2])
await timeline.waitForPart("prt_transport_split")
await expect(page.locator('[data-timeline-part-id="prt_transport_split"]')).toContainText(
"split snowman \u2603\u2603\u2603",
)
expect(acknowledgement.chunkCount).toBe(4)
})
test("delivers server heartbeat without mutating the timeline", async ({ page }) => {
const timeline = await setupTimeline(page, {
messages: [userMessage(), assistantMessage([textPart("prt_transport_steady", "steady")])],
})
const before = await page.locator("[data-timeline-row]").allTextContents()
await timeline.transport.heartbeat()
await timeline.settle()
expect(await page.locator("[data-timeline-row]").allTextContents()).toEqual(before)
expect(await timeline.transport.connections()).toHaveLength(1)
})
test("reconnects after a clean close", async ({ page }) => {
const timeline = await setupTimeline(page, { eventRetry: 10 })
const first = await timeline.transport.waitForConnection()
await timeline.transport.close()
const second = await timeline.transport.waitForConnection({ after: first.id })
await timeline.transport.send(partUpdated(textPart("prt_transport_close", "after close")))
await timeline.waitForPart("prt_transport_close")
expect(second.id).toBeGreaterThan(first.id)
expect((await timeline.transport.connections())[0]?.endedBy).toBe("close")
})
test("reconnects after a stream error", async ({ page }) => {
const timeline = await setupTimeline(page, { eventRetry: 10 })
const first = await timeline.transport.waitForConnection()
await timeline.transport.error("contract failure")
const second = await timeline.transport.waitForConnection({ after: first.id })
await timeline.transport.send(status("busy"))
await expect.poll(async () => (await timeline.transport.connections()).length).toBe(2)
expect(second.id).toBeGreaterThan(first.id)
expect((await timeline.transport.connections())[0]?.endedBy).toBe("error")
})
test("records event IDs and reconnect Last-Event-ID headers", async ({ page }) => {
const timeline = await setupTimeline(page, { eventRetry: 10 })
const first = await timeline.transport.send(partUpdated(textPart("prt_transport_id", "event with id")), {
id: "timeline-event-7",
})
await timeline.waitForPart("prt_transport_id")
await timeline.transport.error("retry with event id")
const connection = await timeline.transport.waitForConnection({ after: first.connectionID })
expect(first.eventID).toBe("timeline-event-7")
expect(connection.headers["last-event-id"]).toBe("timeline-event-7")
})
test("passes through non-event fetches", async ({ page }) => {
const timeline = await setupTimeline(page)
const health = await page.evaluate(async () => {
const response = await fetch("/global/health")
return response.json()
})
expect(health).toEqual({ healthy: true })
expect(await timeline.transport.connections()).toHaveLength(1)
})
@@ -145,7 +145,7 @@ async function configurePage(page: Page) {
}),
)
localStorage.setItem(
"opencode.global.dat:tabs",
"opencode.window.browser.dat:tabs",
JSON.stringify(sessionIDs.map((sessionId) => ({ type: "session", server, dirBase64, sessionId }))),
)
},
@@ -0,0 +1,200 @@
import { base64Encode } from "@opencode-ai/core/util/encode"
import { expect, test, type Page } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectSessionTitle } from "../utils/waits"
const directory = "C:/OpenCode/SubagentNavigation"
const projectID = "proj_subagent_navigation"
const parentID = "ses_subagent_parent"
const childID = "ses_subagent_child"
const parentTitle = "Parent session"
const childTitle = "Subagent child session"
// Child session pages derive their heading from the task part that spawned them.
const taskDescription = "Inspect child navigation"
type EventPayload = { directory: string; payload: Record<string, unknown> }
test.use({ viewport: { width: 1440, height: 900 } })
test("navigates to a subagent child session missing from the session list", async ({ page }) => {
await setup(page)
await openChildFromParent(page)
await expectSessionTitle(page, taskDescription)
await expect(page.getByRole("heading", { name: parentTitle })).toHaveCount(0)
const titlebarRight = page.locator("#opencode-titlebar-right")
await expect(titlebarRight.getByRole("button", { name: "Toggle review" })).toHaveCount(1)
})
test("shows the not found fallback when the viewed session is deleted", async ({ page }) => {
const events: EventPayload[] = []
await setup(page, () => events.splice(0, 1))
await openChildFromParent(page)
await expectSessionTitle(page, taskDescription)
events.push({
directory,
payload: { type: "session.deleted", properties: { info: childSession() } },
})
await expect(page.getByText("This session cannot be found")).toBeVisible()
await expect(page.getByRole("button", { name: "Close Tab" })).toBeVisible()
await expect(page.getByRole("heading", { name: taskDescription })).toHaveCount(0)
})
async function setup(page: Page, events?: () => EventPayload[]) {
await mockOpenCodeServer(page, {
directory,
project: {
id: projectID,
worktree: directory,
vcs: "git",
name: "subagent-navigation",
time: { created: 1700000000000, updated: 1700000000000 },
sandboxes: [],
},
provider: {
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" },
},
sessions: [session(parentID, parentTitle, 1700000000000), childSession()],
pageMessages: (sessionID) => ({ items: sessionID === parentID ? parentMessages() : [] }),
events,
eventRetry: events ? 16 : undefined,
})
// The child session resolves via /session/:id but is absent from the /session list,
// matching a subagent session that has not been loaded into the list cache yet.
await page.route(
(url) => url.pathname === "/session" && url.port === (process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"),
(route) =>
route.fulfill({
status: 200,
contentType: "application/json",
headers: { "access-control-allow-origin": "*" },
body: JSON.stringify([session(parentID, parentTitle, 1700000000000)]),
}),
)
await configurePage(page)
}
async function openChildFromParent(page: Page) {
await page.goto(sessionHref(parentID))
await expectSessionTitle(page, parentTitle)
const card = page.locator(`a[href="${sessionHref(childID)}"]`)
await expect(card).toBeVisible()
await card.click()
await expect(page).toHaveURL(new RegExp(`/server/.+/session/${childID}$`), { timeout: 15_000 })
}
function session(id: string, title: string, created: number, extra?: Record<string, unknown>) {
return {
id,
slug: id,
projectID,
directory,
title,
version: "dev",
time: { created, updated: created },
...extra,
}
}
function childSession() {
return session(childID, childTitle, 1700000001000, { parentID })
}
function parentMessages() {
const userID = "msg_user_0001"
const assistantID = "msg_assistant_0001"
return [
{
info: {
id: userID,
sessionID: parentID,
role: "user",
time: { created: 1700000000000 },
agent: "build",
model: { providerID: "opencode", modelID: "claude-opus-4-6" },
},
parts: [
{
id: "prt_user_text_0001",
sessionID: parentID,
messageID: userID,
type: "text",
text: "Delegate work to a subagent",
},
],
},
{
info: {
id: assistantID,
sessionID: parentID,
role: "assistant",
time: { created: 1700000001000, completed: 1700000002000 },
parentID: userID,
modelID: "claude-opus-4-6",
providerID: "opencode",
mode: "build",
agent: "build",
path: { cwd: directory, root: directory },
cost: 0.01,
tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } },
finish: "stop",
},
parts: [
{
id: "prt_tool_task_0001",
sessionID: parentID,
messageID: assistantID,
type: "tool",
callID: "call_task_0001",
tool: "task",
state: {
status: "completed",
input: { description: taskDescription, subagent_type: "explore" },
output: "Subagent finished",
title: taskDescription,
metadata: { sessionId: childID },
time: { start: 1700000001000, end: 1700000002000 },
},
},
],
},
]
}
async function configurePage(page: Page) {
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
await page.addInitScript(
({ directory, server, sessionId }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem(
"opencode.global.dat:server",
JSON.stringify({
projects: { local: [{ worktree: directory, expanded: true }] },
lastProject: { local: directory },
}),
)
localStorage.setItem("opencode.window.browser.dat:tabs", JSON.stringify([{ type: "session", server, sessionId }]))
},
{ directory, server, sessionId: parentID },
)
}
function sessionHref(sessionID: string) {
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
return `/server/${base64Encode(server)}/session/${sessionID}`
}
@@ -0,0 +1,108 @@
import { expect, test, type Page, type Route } from "@playwright/test"
import { base64Encode } from "@opencode-ai/core/util/encode"
const server = "http://127.0.0.1:4096"
const sessionA = session("ses_tab_a", "Tab A session")
const sessionB = session("ses_tab_b", "Tab B session")
test("pressing mouse down on a tab navigates before mouse up", async ({ page }) => {
await mockServer(page)
await page.addInitScript(
({ server, sessionA, sessionB }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem(
"opencode.window.browser.dat:tabs",
JSON.stringify([
{ type: "session", server, sessionId: sessionA },
{ type: "session", server, sessionId: sessionB },
]),
)
},
{ server, sessionA: sessionA.id, sessionB: sessionB.id },
)
const hrefA = `/server/${base64Encode(server)}/session/${sessionA.id}`
const hrefB = `/server/${base64Encode(server)}/session/${sessionB.id}`
await page.goto(hrefA)
await expect(page.getByText(sessionA.title).first()).toBeVisible()
const linkB = page.locator(`a[data-titlebar-tab-link][href="${hrefB}"]`)
await expect(linkB).toBeVisible()
const box = await linkB.boundingBox()
if (!box) throw new Error("tab link has no bounding box")
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2)
await page.mouse.down()
// Navigation must happen on mousedown, before the button is released.
await expect(page).toHaveURL(new RegExp(`${hrefB.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`))
await page.mouse.up()
await expect(page).toHaveURL(new RegExp(`${hrefB.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`))
})
function session(id: string, title: string) {
return {
id,
slug: id,
projectID: "project-tabs",
directory: "C:/tab-project",
title,
version: "dev",
time: { created: 1, updated: 1 },
}
}
async function mockServer(page: Page) {
const sessions = [sessionA, sessionB]
await page.route("**/*", async (route) => {
const url = new URL(route.request().url())
if (url.origin !== server) return route.fallback()
if (url.pathname === "/global/event" || url.pathname === "/event") return sse(route)
if (url.pathname === "/global/health") return json(route, { healthy: true })
if (url.pathname === "/session") return json(route, sessions)
const byId = sessions.find((item) => url.pathname === `/session/${item.id}`)
if (byId) return json(route, byId)
if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404)
if (/^\/session\/[^/]+\/message$/.test(url.pathname)) return json(route, [])
if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(url.pathname)) return json(route, [])
if (["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/diff"].includes(url.pathname))
return json(route, [])
if (["/global/config", "/config", "/provider/auth", "/mcp", "/session/status"].includes(url.pathname))
return json(route, {})
if (url.pathname === "/provider")
return json(route, { all: [], connected: [], default: { providerID: "", modelID: "" } })
if (url.pathname === "/agent") return json(route, [{ name: "build", mode: "primary" }])
if (url.pathname === "/project" || url.pathname === "/project/current") {
const project = {
id: sessionA.projectID,
worktree: sessionA.directory,
vcs: "git",
time: { created: 1, updated: 1 },
sandboxes: [],
}
return json(route, url.pathname === "/project" ? [project] : project)
}
if (url.pathname === "/path")
return json(route, {
state: sessionA.directory,
config: sessionA.directory,
worktree: sessionA.directory,
directory: sessionA.directory,
home: sessionA.directory,
})
if (url.pathname === "/vcs") return json(route, { branch: "main", default_branch: "main" })
return json(route, {})
})
}
function json(route: Route, body: unknown, status = 200) {
return route.fulfill({
status,
contentType: "application/json",
headers: { "access-control-allow-origin": "*" },
body: JSON.stringify(body),
})
}
function sse(route: Route) {
return route.fulfill({ status: 200, contentType: "text/event-stream", body: ": ok\n\n" })
}
@@ -0,0 +1,89 @@
import { base64Encode } from "@opencode-ai/core/util/encode"
import { expect, test } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectSessionTitle } from "../utils/waits"
const directory = "C:/OpenCode/TerminalComposerFocus"
const projectID = "proj_terminal_composer_focus"
const sessionID = "ses_terminal_composer_focus"
const ptyID = "pty_terminal_composer_focus"
test.use({ viewport: { width: 1440, height: 900 } })
test("routes typing to the composer unless the open terminal is focused", async ({ page }) => {
await mockOpenCodeServer(page, {
directory,
project: {
id: projectID,
worktree: directory,
vcs: "git",
name: "terminal-composer-focus",
time: { created: 1700000000000, updated: 1700000000000 },
sandboxes: [],
},
provider: {
all: [
{
id: "opencode",
name: "OpenCode",
models: { test: { id: "test", name: "Test", limit: { context: 200_000 } } },
},
],
connected: ["opencode"],
default: { providerID: "opencode", modelID: "test" },
},
sessions: [
{
id: sessionID,
slug: "terminal-composer-focus",
projectID,
directory,
title: "Terminal composer focus",
version: "dev",
time: { created: 1700000000000, updated: 1700000000000 },
},
],
pageMessages: () => ({ items: [] }),
})
await page.route("**/pty", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ id: ptyID, title: "Terminal 1" }),
}),
)
await page.route(`**/pty/${ptyID}`, (route) =>
route.fulfill({ status: 200, contentType: "application/json", body: "{}" }),
)
await page.route(`**/pty/${ptyID}/connect-token*`, (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
headers: { "access-control-allow-origin": "*" },
body: JSON.stringify({ ticket: "e2e-ticket" }),
}),
)
await page.routeWebSocket(new RegExp(`/pty/${ptyID}/connect`), () => undefined)
await page.addInitScript(() => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
})
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
await expectSessionTitle(page, "Terminal composer focus")
const composer = page.locator('[data-component="prompt-input"]')
const terminal = page.locator('[data-component="terminal"]')
await page.keyboard.press("Control+Backquote")
await expect(terminal).toBeVisible()
await expect.poll(() => terminal.evaluate((element) => element.contains(document.activeElement))).toBe(true)
await page.keyboard.type("x")
await expect(composer).toHaveText("")
await page.waitForTimeout(300)
await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur())
await page.keyboard.type("a")
await expect(composer).toBeFocused()
await expect(composer).toHaveText("a")
})
@@ -7,7 +7,7 @@ const projectID = "proj_hidden_terminal_regression"
const sessionID = "ses_hidden_terminal_regression"
const title = "Hidden terminal regression"
test("unmounts the terminal renderer while the pane is hidden", async ({ page }) => {
test("unmounts the terminal panel while it is hidden", async ({ page }) => {
await page.setViewportSize({ width: 1400, height: 900 })
await mockOpenCodeServer(page, {
directory,
@@ -64,13 +64,14 @@ test("unmounts the terminal renderer while the pane is hidden", async ({ page })
await expect(page.locator('[data-component="terminal"]')).toBeVisible()
await page.keyboard.press("Control+Backquote")
await expect(panel).toHaveAttribute("aria-hidden", "true")
await expect(panel).toHaveCount(0)
await expect(page.locator('[data-component="terminal"]')).toHaveCount(0)
await page.setViewportSize({ width: 1200, height: 700 })
await expect(page.locator('[data-component="terminal"]')).toHaveCount(0)
await page.keyboard.press("Control+Backquote")
await expect(panel).toBeVisible()
await expect(page.locator('[data-component="terminal"]')).toBeVisible()
})
@@ -0,0 +1,145 @@
import { base64Encode } from "@opencode-ai/core/util/encode"
import { expect, test, type Page } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectSessionTitle } from "../utils/waits"
const directory = "C:/OpenCode/TerminalTabSwitch"
const projectID = "proj_terminal_tab_switch"
const sessionA = "ses_terminal_tab_a"
const sessionB = "ses_terminal_tab_b"
const titleA = "Alpha session"
const titleB = "Beta session"
const ptyID = "pty_tab_switch"
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
// Marks the terminal DOM node so a remount (fresh node) is detectable.
const PROBE = "original"
test.use({ viewport: { width: 1440, height: 900 } })
// Terminals are workspace-scoped: switching between session tabs in the same
// workspace must keep the terminal mounted and its PTY connection open instead
// of tearing it down and reconnecting.
test("keeps the terminal session alive when switching session tabs in a workspace", async ({ page }) => {
const connections = await setup(page)
await page.goto(sessionHref(sessionA))
await expectSessionTitle(page, titleA)
await page.keyboard.press("Control+Backquote")
const terminal = page.locator('[data-component="terminal"]')
await expect(terminal).toBeVisible()
await expect.poll(() => connections.length).toBe(1)
await writeProbe(page)
await switchTab(page, titleB)
await expectSessionTitle(page, titleB)
await expect(terminal).toBeVisible()
expect(await readProbe(page)).toBe(PROBE)
expect(connections.length).toBe(1)
await switchTab(page, titleA)
await expectSessionTitle(page, titleA)
await expect(terminal).toBeVisible()
expect(await readProbe(page)).toBe(PROBE)
expect(connections.length).toBe(1)
})
type Probed = HTMLElement & { __e2eProbe?: string }
async function switchTab(page: Page, title: string) {
await page.locator("[data-titlebar-tab-slot]", { hasText: title }).click()
}
async function writeProbe(page: Page) {
await page.locator('[data-component="terminal"]').evaluate((el, probe) => {
;(el as Probed).__e2eProbe = probe
}, PROBE)
}
async function readProbe(page: Page) {
return page.locator('[data-component="terminal"]').evaluate((el) => (el as Probed).__e2eProbe)
}
async function setup(page: Page) {
await mockOpenCodeServer(page, {
directory,
project: {
id: projectID,
worktree: directory,
vcs: "git",
name: "terminal-tab-switch",
time: { created: 1700000000000, updated: 1700000000000 },
sandboxes: [],
},
provider: {
all: [
{
id: "opencode",
name: "OpenCode",
models: { test: { id: "test", name: "Test", limit: { context: 200_000 } } },
},
],
connected: ["opencode"],
default: { providerID: "opencode", modelID: "test" },
},
sessions: [session(sessionA, titleA, 1700000000000), session(sessionB, titleB, 1700000001000)],
pageMessages: () => ({ items: [] }),
})
await page.route("**/pty", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ id: ptyID, title: "Terminal 1" }),
}),
)
await page.route(`**/pty/${ptyID}`, (route) =>
route.fulfill({ status: 200, contentType: "application/json", body: "{}" }),
)
await page.route(`**/pty/${ptyID}/connect-token*`, (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
headers: { "access-control-allow-origin": "*" },
body: JSON.stringify({ ticket: "e2e-ticket" }),
}),
)
const connections: string[] = []
await page.routeWebSocket(new RegExp(`/pty/${ptyID}/connect`), (ws) => {
connections.push(ws.url())
})
await page.addInitScript(
({ directory, server, sessions }) => {
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem(
"opencode.global.dat:server",
JSON.stringify({
projects: { local: [{ worktree: directory, expanded: true }] },
lastProject: { local: directory },
}),
)
localStorage.setItem(
"opencode.window.browser.dat:tabs",
JSON.stringify(sessions.map((sessionId: string) => ({ type: "session", server, sessionId }))),
)
},
{ directory, server, sessions: [sessionA, sessionB] },
)
return connections
}
function session(id: string, title: string, created: number) {
return {
id,
slug: id,
projectID,
directory,
title,
version: "dev",
time: { created, updated: created },
}
}
function sessionHref(sessionID: string) {
return `/server/${base64Encode(server)}/session/${sessionID}`
}
@@ -127,7 +127,7 @@ test.describe("smoke: session timeline", () => {
await page.addInitScript(
({ dirBase64, sourceID, targetID }) => {
localStorage.setItem(
"opencode.global.dat:tabs",
"opencode.window.browser.dat:tabs",
JSON.stringify(
[sourceID, targetID].map((sessionId) => ({
type: "session",
@@ -253,7 +253,7 @@ test.describe("smoke: session timeline", () => {
await page.addInitScript(
({ dirBase64, sourceID, targetID }) => {
localStorage.setItem(
"opencode.global.dat:tabs",
"opencode.window.browser.dat:tabs",
JSON.stringify(
[sourceID, targetID].map((sessionId) => ({
type: "session",
+9 -1
View File
@@ -5,5 +5,13 @@
"rootDir": "..",
"types": ["node", "bun"]
},
"include": ["./**/*.ts"]
"include": [
"./performance/timeline-stability/**/*.spec.ts",
"./performance/timeline-stability/fixture.test.ts",
"./performance/timeline-stability/fixture.ts",
"./performance/unit/visual-stability.test.ts",
"./regression/new-session-panel-corner.spec.ts",
"./regression/session-timeline-context-resize.spec.ts",
"./utils/**/*.ts"
]
}
+45 -2
View File
@@ -1,7 +1,7 @@
import type { Page, Route } from "@playwright/test"
const emptyList = new Set(["/skill", "/command", "/lsp", "/formatter", "/vcs/status", "/vcs/diff"])
const emptyObject = new Set(["/global/config", "/config", "/provider/auth", "/mcp", "/session/status"])
const emptyObject = new Set(["/global/config", "/config", "/provider/auth", "/mcp", "/experimental/resource"])
export interface MockServerConfig {
provider: unknown
@@ -11,12 +11,19 @@ export interface MockServerConfig {
pageMessages: (sessionId: string, limit: number, before?: string) => { items: unknown[]; cursor?: string }
vcsDiff?: unknown[]
messageDelay?: number
beforeMessagesResponse?: (input: { sessionID: string; before?: string }) => Promise<void>
onMessages?: (input: { sessionID: string; before?: string; phase: "start" | "end" }) => void
message?: (sessionID: string, messageID: string) => unknown
onMessage?: (input: { sessionID: string; messageID: string }) => void
events?: () => unknown[]
eventRetry?: number
todos?: (sessionID: string) => unknown[]
permissions?: unknown[] | (() => unknown[])
questions?: unknown[] | (() => unknown[])
fileList?: (path: string) => unknown | Promise<unknown>
fileContent?: (path: string) => unknown | Promise<unknown>
findFiles?: (input: { query: string; dirs?: string; limit?: number }) => unknown
sessionStatus?: unknown
}
export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
@@ -49,11 +56,34 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
const path = url.pathname
if (path === "/global/event" || path === "/event") return sse(route, config.events?.(), config.eventRetry)
if (path === "/global/health") return json(route, { healthy: true })
if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: false })
if (path === "/permission")
return json(route, typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? []))
if (path === "/question")
return json(route, typeof config.questions === "function" ? config.questions() : (config.questions ?? []))
if (path === "/session/status") return json(route, config.sessionStatus ?? {})
if (path === "/vcs/diff" && config.vcsDiff) return json(route, config.vcsDiff)
if (path === "/file" && config.fileList)
return json(route, await config.fileList(url.searchParams.get("path") ?? ""))
if (path === "/file/content" && config.fileContent)
return json(route, await config.fileContent(url.searchParams.get("path") ?? ""))
if (path === "/find/file" && config.findFiles)
return json(
route,
await config.findFiles({
query: url.searchParams.get("query") ?? "",
dirs: url.searchParams.get("dirs") ?? undefined,
limit: url.searchParams.has("limit") ? Number(url.searchParams.get("limit")) : undefined,
}),
)
if (path === "/api/reference")
return json(route, {
location: {
directory: config.directory,
project: { id: (config.project as { id?: string }).id, directory: config.directory },
},
data: [],
})
if (emptyObject.has(path)) return json(route, {})
if (emptyList.has(path)) return json(route, [])
if (path in staticRoutes) return json(route, staticRoutes[path])
@@ -64,6 +94,18 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
return json(route, session ?? {})
}
const projectMatch = path.match(/^\/project\/([^/]+)$/)
if (projectMatch) return json(route, config.project)
const messageMatch = path.match(/^\/session\/([^/]+)\/message\/([^/]+)$/)
if (messageMatch) {
config.onMessage?.({ sessionID: messageMatch[1]!, messageID: messageMatch[2]! })
if (config.messageDelay !== undefined) await new Promise((resolve) => setTimeout(resolve, config.messageDelay))
const message = config.message?.(messageMatch[1]!, messageMatch[2]!)
if (message === undefined) return json(route, { error: "Message not found" }, undefined, 404)
return json(route, message)
}
const todoMatch = path.match(/^\/session\/([^/]+)\/todo$/)
if (todoMatch) return json(route, config.todos?.(todoMatch[1]!) ?? [])
if (/^\/session\/[^/]+\/(children|diff)$/.test(path)) return json(route, [])
@@ -74,7 +116,8 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
const before = token ? cursors.get(token) : undefined
if (token && !before) return json(route, { error: "Invalid cursor" }, undefined, 400)
config.onMessages?.({ sessionID: messagesMatch[1], before, phase: "start" })
if (config.messageDelay) await new Promise((resolve) => setTimeout(resolve, config.messageDelay))
await config.beforeMessagesResponse?.({ sessionID: messagesMatch[1]!, before })
if (config.messageDelay !== undefined) await new Promise((resolve) => setTimeout(resolve, config.messageDelay))
const limit = Number(url.searchParams.get("limit") ?? 80)
const pageData = config.pageMessages(messagesMatch[1], limit, before)
config.onMessages?.({ sessionID: messagesMatch[1], before, phase: "end" })
+284
View File
@@ -0,0 +1,284 @@
import type { Page } from "@playwright/test"
export type SseConnectionRecord = {
id: number
url: string
path: "/global/event" | "/event"
headers: Record<string, string>
openedAt: number
endedAt?: number
endedBy?: "close" | "disconnect" | "error" | "abort"
error?: string
}
export type SseDeliveryAcknowledgement = {
deliveryID: number
connectionID: number
bytes: number
chunkCount: number
deliveredAt: number
eventID?: string
}
export type SseEventOptions = {
id?: string
event?: string
retry?: number
marker?: string
}
export type SseTransport<T> = {
server: string
waitForConnection(options?: { after?: number; timeout?: number }): Promise<SseConnectionRecord>
send(payload: T, options?: SseEventOptions): Promise<SseDeliveryAcknowledgement>
burst(payloads: readonly T[], options?: readonly SseEventOptions[]): Promise<SseDeliveryAcknowledgement[]>
split(payload: T, cuts: readonly number[], options?: SseEventOptions): Promise<SseDeliveryAcknowledgement>
heartbeat(options?: SseEventOptions): Promise<SseDeliveryAcknowledgement>
writeRaw(value: string | Uint8Array, cuts?: readonly number[], marker?: string): Promise<SseDeliveryAcknowledgement>
close(): Promise<void>
disconnect(message?: string): Promise<void>
error(message?: string): Promise<void>
connections(): Promise<SseConnectionRecord[]>
acknowledgements(): Promise<SseDeliveryAcknowledgement[]>
}
type BrowserCommand<T> =
| { type: "send"; deliveries: { payload: T; options?: SseEventOptions }[]; burst: boolean; cuts?: number[] }
| { type: "raw"; bytes: number[]; cuts?: number[]; marker?: string }
| { type: "end"; mode: "close" | "disconnect" | "error"; message?: string }
| { type: "connections" }
| { type: "acknowledgements" }
type BrowserTransport = Window & {
__testSseTransport?: {
command: (command: BrowserCommand<unknown>) => unknown
}
}
export async function installSseTransport<T>(
page: Page,
options: { server: string; retry?: number },
): Promise<SseTransport<T>> {
const server = new URL(options.server).origin
await page.addInitScript(
({ server, retry }) => {
type Connection = SseConnectionRecord & { controller: ReadableStreamDefaultController<Uint8Array> }
type ProbeWindow = Window & {
__visualStabilityProbe?: { startedAt: number; markers: { at: number; label: string }[] }
}
const originalFetch = window.fetch.bind(window)
const connections: Connection[] = []
const acknowledgements: SseDeliveryAcknowledgement[] = []
const encoder = new TextEncoder()
let nextConnectionID = 0
let nextDeliveryID = 0
const current = () => connections.findLast((connection) => connection.endedAt === undefined)
const chunks = (bytes: Uint8Array, cuts?: readonly number[]) => {
const boundaries = [...new Set(cuts ?? [])]
.filter((cut) => Number.isInteger(cut) && cut > 0 && cut < bytes.byteLength)
.sort((a, b) => a - b)
return [0, ...boundaries].map((start, index) => bytes.slice(start, boundaries[index] ?? bytes.byteLength))
}
const marker = (label?: string) => {
if (!label) return
const probe = (window as ProbeWindow).__visualStabilityProbe
if (!probe) return
probe.markers.push({ at: performance.now() - probe.startedAt, label })
}
const frame = (payload: unknown, eventOptions: SseEventOptions = {}) =>
[
eventOptions.event === undefined ? "" : `event: ${eventOptions.event}\n`,
eventOptions.id === undefined ? "" : `id: ${eventOptions.id}\n`,
eventOptions.retry === undefined ? "" : `retry: ${eventOptions.retry}\n`,
`data: ${JSON.stringify(payload)}\n\n`,
].join("")
const acknowledge = (
connection: Connection,
bytes: number,
chunkCount: number,
eventID?: string,
): SseDeliveryAcknowledgement => {
const acknowledgement = {
deliveryID: ++nextDeliveryID,
connectionID: connection.id,
bytes,
chunkCount,
deliveredAt: performance.now(),
...(eventID === undefined ? {} : { eventID }),
}
acknowledgements.push(acknowledgement)
return acknowledgement
}
const end = (mode: "close" | "disconnect" | "error", message?: string) => {
const connection = current()
if (!connection) throw new Error("SSE transport has no active connection")
connection.endedAt = performance.now()
connection.endedBy = mode
if (message) connection.error = message
if (mode === "close") {
connection.controller.close()
return
}
const error = new DOMException(
message ?? "SSE connection disconnected",
mode === "error" ? "Error" : "NetworkError",
)
connection.controller.error(error)
}
const command = (input: BrowserCommand<unknown>) => {
if (input.type === "connections")
return connections.map(({ controller: _controller, ...connection }) => connection)
if (input.type === "acknowledgements") return acknowledgements
if (input.type === "end") return end(input.mode, input.message)
const connection = current()
if (!connection) throw new Error("SSE transport has no active connection")
if (input.type === "raw") {
marker(input.marker)
const output = chunks(new Uint8Array(input.bytes), input.cuts)
output.forEach((chunk) => connection.controller.enqueue(chunk))
return acknowledge(connection, input.bytes.length, output.length)
}
const encoded = input.deliveries.map((delivery) => ({
delivery,
bytes: encoder.encode(frame(delivery.payload, delivery.options)),
}))
encoded.forEach((item) => marker(item.delivery.options?.marker))
if (input.burst) {
const bytes = encoder.encode(
encoded.map((item) => frame(item.delivery.payload, item.delivery.options)).join(""),
)
connection.controller.enqueue(bytes)
return encoded.map((item) => acknowledge(connection, item.bytes.byteLength, 1, item.delivery.options?.id))
}
const output = chunks(encoded[0]!.bytes, input.cuts)
output.forEach((chunk) => connection.controller.enqueue(chunk))
return acknowledge(connection, encoded[0]!.bytes.byteLength, output.length, encoded[0]!.delivery.options?.id)
}
;(window as BrowserTransport).__testSseTransport = { command }
const fetch = (input: RequestInfo | URL, init?: RequestInit) => {
const request = new Request(input, init)
const url = new URL(request.url)
if (url.origin !== server || (url.pathname !== "/global/event" && url.pathname !== "/event"))
return originalFetch(input, init)
const id = ++nextConnectionID
const record = {
id,
url: url.href,
path: url.pathname,
headers: Object.fromEntries(request.headers.entries()),
openedAt: performance.now(),
} as Connection
const stream = new ReadableStream<Uint8Array>({
start(controller) {
record.controller = controller
connections.push(record)
if (retry !== undefined) controller.enqueue(encoder.encode(`retry: ${retry}\n\n`))
request.signal.addEventListener(
"abort",
() => {
if (record.endedAt !== undefined) return
record.endedAt = performance.now()
record.endedBy = "abort"
controller.error(request.signal.reason ?? new DOMException("The operation was aborted", "AbortError"))
},
{ once: true },
)
},
cancel() {
if (record.endedAt !== undefined) return
record.endedAt = performance.now()
record.endedBy = "disconnect"
},
})
return Promise.resolve(
new Response(stream, {
status: 200,
headers: {
"cache-control": "no-cache",
"content-type": "text/event-stream",
},
}),
)
}
Object.defineProperty(window, "fetch", { configurable: true, writable: true, value: fetch })
},
{ server, retry: options.retry },
)
const command = <Result>(input: BrowserCommand<T>) =>
page.evaluate((input) => {
const transport = (window as BrowserTransport).__testSseTransport
if (!transport) throw new Error("SSE transport was not installed before page load")
return transport.command(input as BrowserCommand<unknown>)
}, input) as Promise<Result>
return {
server,
async waitForConnection(input = {}) {
await page.waitForFunction(
(after) => {
const transport = (window as BrowserTransport).__testSseTransport
const connections = transport?.command({ type: "connections" }) as SseConnectionRecord[] | undefined
return connections?.some((connection) => connection.id > after)
},
input.after ?? 0,
{ timeout: input.timeout },
)
return (await command<SseConnectionRecord[]>({ type: "connections" })).findLast(
(connection) => connection.id > (input.after ?? 0),
)!
},
send(payload, eventOptions) {
return command({ type: "send", deliveries: [{ payload, options: eventOptions }], burst: false })
},
burst(payloads, eventOptions = []) {
return command({
type: "send",
deliveries: payloads.map((payload, index) => ({ payload, options: eventOptions[index] })),
burst: true,
})
},
split(payload, cuts, eventOptions) {
return command({ type: "send", deliveries: [{ payload, options: eventOptions }], burst: false, cuts: [...cuts] })
},
heartbeat(eventOptions) {
return command({
type: "send",
deliveries: [
{
payload: { directory: "global", payload: { type: "server.heartbeat", properties: {} } } as T,
options: eventOptions,
},
],
burst: false,
})
},
writeRaw(value, cuts, marker) {
return command({
type: "raw",
bytes: Array.from(typeof value === "string" ? new TextEncoder().encode(value) : value),
cuts: cuts ? [...cuts] : undefined,
marker,
})
},
close() {
return command({ type: "end", mode: "close" })
},
disconnect(message) {
return command({ type: "end", mode: "disconnect", message })
},
error(message) {
return command({ type: "end", mode: "error", message })
},
connections() {
return command({ type: "connections" })
},
acknowledgements() {
return command({ type: "acknowledgements" })
},
}
}
@@ -0,0 +1,54 @@
import type { Page, TestInfo } from "@playwright/test"
import { analyzeVisualObservations, analyzeVisualTraceByMarker } from "./visual-stability/analyzer"
import { legacyVisualPlan, type LegacyVisualStabilityOptions } from "./visual-stability/invariant"
import type { CapturedFrame, VisualStabilityTrace } from "./visual-stability/model"
import { markVisualProbe, startVisualProbe, stopVisualProbe } from "./visual-stability/probe"
import type { VisualRegionDefinition } from "./visual-stability/regions"
import { reportVisualStability } from "./visual-stability/reporter"
export * from "./visual-stability/index"
const capturedFrames = Symbol("capturedFrames")
export async function startVisualStabilityProbe(page: Page, regions: Record<string, VisualRegionDefinition>) {
await startVisualProbe(page, regions)
}
export async function stopVisualStabilityProbe(page: Page) {
const result = await stopVisualProbe(page)
const trace: VisualStabilityTrace = { markers: result.markers, samples: result.samples }
Object.defineProperty(trace, capturedFrames, { value: result.frames })
return trace
}
export async function markVisualStability(page: Page, label: string) {
await markVisualProbe(page, label)
}
export function analyzeVisualStability(trace: VisualStabilityTrace, options: LegacyVisualStabilityOptions = {}) {
return analyzeVisualObservations(trace.samples, legacyVisualPlan(options))
}
export function analyzeVisualStabilityByMarker(
trace: VisualStabilityTrace,
options: LegacyVisualStabilityOptions = {},
) {
return analyzeVisualTraceByMarker(trace, legacyVisualPlan(options))
}
export async function expectVisualStability(
testInfo: TestInfo,
name: string,
trace: VisualStabilityTrace,
options: LegacyVisualStabilityOptions = {},
) {
await reportVisualStability(
testInfo,
name,
{
...trace,
frames: (trace as VisualStabilityTrace & { [capturedFrames]?: CapturedFrame[] })[capturedFrames] ?? [],
},
legacyVisualPlan(options),
)
}
@@ -0,0 +1,209 @@
import type { VisualInvariant, VisualPlan } from "./invariant"
import type { VisualObservation, VisualStabilityTrace } from "./model"
export function analyzeVisualObservations<RegionName extends string>(
observations: readonly VisualObservation<RegionName>[],
plan: VisualPlan<RegionName>,
) {
const issues: string[] = []
const invariants = plan.invariants
const names = [...new Set(observations.flatMap((sample) => Object.keys(sample.regions) as RegionName[]))]
const required = regions(invariants, "required")
const continuousAny = invariants.filter(
(invariant): invariant is Extract<VisualInvariant<RegionName>, { type: "continuous-any" }> =>
invariant.type === "continuous-any",
)
const unique = new Set(regions(invariants, "unique"))
const stable = new Set(regions(invariants, "stable"))
const fixed = invariants.filter(
(invariant): invariant is Extract<VisualInvariant<RegionName>, { type: "fixed" }> => invariant.type === "fixed",
)
const opacity = invariants.filter(
(invariant): invariant is Extract<VisualInvariant<RegionName>, { type: "opacity" }> => invariant.type === "opacity",
)
const continuity = invariants.filter(
(invariant): invariant is Extract<VisualInvariant<RegionName>, { type: "continuity" }> =>
invariant.type === "continuity",
)
const motion = invariants.filter(
(invariant): invariant is Extract<VisualInvariant<RegionName>, { type: "motion" }> => invariant.type === "motion",
)
const labelStability = invariants.filter(
(invariant): invariant is Extract<VisualInvariant<RegionName>, { type: "label-stability" }> =>
invariant.type === "label-stability",
)
for (const name of new Set(required)) {
if (!observations.some((sample) => sample.regions[name]?.visible)) issues.push(`${name} never rendered`)
}
for (const invariant of continuousAny) {
if (!invariant.regions.some((name) => observations.some((sample) => sample.regions[name]?.visible)))
issues.push(`${invariant.regions.join(" | ")} never rendered`)
}
for (const name of names) {
const samples = observations.flatMap((observation) => {
const region = observation.regions[name]
if (!region) return []
const clipped =
observation.viewport && (region.bottom <= observation.viewport.top || region.top >= observation.viewport.bottom)
return [{ at: observation.at, ...region, visible: region.visible && !clipped }]
})
const visible = samples.filter((sample) => sample.visible)
if (visible.length === 0) continue
if (unique.has(name)) {
const duplicate = samples.find((sample) => sample.count > 1)
if (duplicate) issues.push(`${name} appeared ${duplicate.count} times at ${Math.round(duplicate.at)}ms`)
}
if (stable.has(name)) {
const identities = [...new Set(visible.map((sample) => sample.node).filter((node) => node > 0))]
if (identities.length > 1) issues.push(`${name} remounted ${identities.length - 1} times`)
}
for (const invariant of fixed.filter((invariant) => includes(invariant.regions, name))) {
const origin = visible[0]
const movement = origin ? Math.max(0, ...visible.map((sample) => Math.abs(sample.top - origin.top))) : 0
if (movement > (invariant.tolerance ?? 1))
issues.push(`${name} moved ${Math.round(movement * 10) / 10}px in the viewport`)
}
for (const invariant of opacity.filter((invariant) => includes(invariant.regions, name))) {
for (const sample of visible) {
if (sample.opacity < (invariant.floor ?? 0.65))
issues.push(`${name} opacity fell to ${sample.opacity} at ${Math.round(sample.at)}ms`)
}
}
if (continuity.some((invariant) => includes(invariant.regions, name))) {
const firstPresent = samples.findIndex((sample) => sample.present)
const lastPresent = samples.findLastIndex((sample) => sample.present)
if (samples.slice(firstPresent, lastPresent + 1).some((sample) => !sample.present))
issues.push(`${name} disappeared between present frames`)
const firstVisible = samples.findIndex((sample) => sample.visible)
const lastVisible = samples.findLastIndex((sample) => sample.visible)
if (
firstVisible >= 0 &&
samples.slice(firstVisible, lastVisible + 1).some((sample) => !sample.visible && sample.inViewport)
)
issues.push(`${name} blanked between visible frames`)
}
for (const invariant of motion.filter((invariant) => includes(invariant.regions, name))) {
for (const metric of ["top", "bottom", "width", "height"] as const) {
const directions = visible
.slice(1)
.map((sample, index) => sample[metric] - visible[index]![metric])
.filter((delta) => Math.abs(delta) > (invariant.tolerance ?? 1))
.map(Math.sign)
const reversals = directions.slice(1).filter((direction, index) => direction !== directions[index]).length
const allowed =
metric === "top" || metric === "bottom"
? (invariant.maxPositionReversals ?? invariant.maxReversals ?? 1)
: (invariant.maxReversals ?? 1)
if (reversals > allowed) issues.push(`${name} ${metric} reversed ${reversals} times`)
}
}
if (labelStability.some((invariant) => includes(invariant.regions, name))) {
const labels = samples
.map((sample) => sample.label)
.filter((label) => label.length > 0)
.filter((label, index, all) => label !== all[index - 1])
if (labels.some((label, index) => labels.indexOf(label) !== index))
issues.push(`${name} label reverted: ${labels.join(" -> ")}`)
}
}
if (invariants.some((invariant) => invariant.type === "preserve-bottom-anchor")) {
const viewports = observations.flatMap((sample) => (sample.viewport ? [sample.viewport] : []))
if (viewports[0] && viewports[0].distanceFromBottom <= 4) {
const lost = viewports.find((viewport) => viewport.distanceFromBottom > 4)
if (lost) issues.push(`bottom anchor moved to ${lost.distanceFromBottom}px`)
}
}
if (invariants.some((invariant) => invariant.type === "acquire-bottom-anchor")) {
const final = observations.findLast((sample) => sample.viewport)?.viewport
if (!final || final.distanceFromBottom > 4)
issues.push(`did not acquire bottom anchor${final ? ` (${final.distanceFromBottom}px away)` : ""}`)
}
for (const invariant of continuousAny) {
const active = observations.map((sample) => invariant.regions.some((name) => sample.regions[name]?.visible))
const first = active.indexOf(true)
const last = active.lastIndexOf(true)
if (first >= 0 && active.slice(first, last + 1).some((value) => !value))
issues.push(`${invariant.regions.join(" | ")} blanked between visible frames`)
}
for (const invariant of invariants.filter(
(item): item is Extract<VisualInvariant<RegionName>, { type: "flow" }> => item.type === "flow",
)) {
for (const [before, after] of invariant.regions
.slice(1)
.map((after, index) => [invariant.regions[index]!, after])) {
let maximum: { overlap: number; at: number } | undefined
let inverted: { at: number } | undefined
for (const sample of observations) {
const first = sample.regions[before]
const second = sample.regions[after]
if (!first?.visible || !second?.visible) continue
if (
sample.viewport &&
(first.bottom <= sample.viewport.top ||
first.top >= sample.viewport.bottom ||
second.bottom <= sample.viewport.top ||
second.top >= sample.viewport.bottom)
)
continue
const overlap = first.bottom - second.top
if (first.top > second.top && !inverted) inverted = { at: sample.at }
if (overlap > (invariant.overlapTolerance ?? 0.5) && (!maximum || overlap > maximum.overlap))
maximum = { overlap, at: sample.at }
}
if (inverted) issues.push(`${before} rendered after ${after} at ${Math.round(inverted.at)}ms`)
if (maximum)
issues.push(
`${before} overlapped ${after} by ${Math.round(maximum.overlap * 10) / 10}px at ${Math.round(maximum.at)}ms`,
)
}
}
return [...new Set(issues)]
}
export function analyzeVisualTraceByMarker<RegionName extends string>(
trace: VisualStabilityTrace<RegionName>,
plan: VisualPlan<RegionName>,
) {
if (trace.markers.length === 0) return analyzeVisualObservations(trace.samples, plan)
const required = [...new Set(plan.markerRequired ?? regions(plan.invariants, "required"))].flatMap((name) =>
trace.samples.some((sample) => sample.regions[name]?.visible) ? [] : [`${name} never rendered`],
)
const withoutRequired = plan.invariants.filter((invariant) => invariant.type !== "required")
const windows = trace.markers.flatMap((marker, index) => {
const end = trace.markers[index + 1]?.at ?? Infinity
const before = trace.samples.findLast((sample) => sample.at < marker.at)
const samples = [
...(before ? [before] : []),
...trace.samples.filter((sample) => sample.at >= marker.at && sample.at < end),
]
if (samples.length < 2) return []
return analyzeVisualObservations(samples, { ...plan, perMarker: false, invariants: withoutRequired }).map(
(issue) => `${marker.label}: ${issue}`,
)
})
const aggregateMotion =
plan.aggregateMotion === false
? []
: analyzeVisualObservations(trace.samples, {
invariants: plan.invariants.filter((invariant) => invariant.type === "motion"),
}).filter((issue) => / (?:top|bottom|width|height) reversed \d+ times$/.test(issue))
return [...new Set([...required, ...aggregateMotion, ...windows])]
}
function regions<RegionName extends string, Type extends VisualInvariant<RegionName>["type"]>(
invariants: readonly VisualInvariant<RegionName>[],
type: Type,
) {
return invariants.flatMap((invariant) =>
invariant.type === type && "regions" in invariant && invariant.regions !== "all" ? [...invariant.regions] : [],
) as RegionName[]
}
function includes<RegionName extends string>(regions: readonly RegionName[] | "all", name: RegionName) {
return regions === "all" || regions.includes(name)
}
@@ -0,0 +1,51 @@
import type { CDPSession, Page } from "@playwright/test"
import type { CapturedFrame } from "./model"
export type VisualCapture = {
session: CDPSession
frames: CapturedFrame[]
startedAtEpoch: number
running: boolean
capture: Promise<void>
}
export async function startVisualCapture(page: Page, startedAtEpoch: number) {
if (process.env.OPENCODE_STABILITY_CAPTURE !== "1") return
const session = await page.context().newCDPSession(page)
await session.send("Page.enable")
const recording: VisualCapture = {
session,
frames: [],
startedAtEpoch,
running: true,
capture: Promise.resolve(),
}
recording.capture = (async () => {
try {
while (recording.running && recording.frames.length < 900) {
const frame = await session.send("Page.captureScreenshot", {
format: "jpeg",
quality: 80,
captureBeyondViewport: false,
optimizeForSpeed: true,
})
recording.frames.push({ at: Date.now() - recording.startedAtEpoch, data: frame.data })
await new Promise((resolve) => setTimeout(resolve, 50))
}
} catch {
recording.running = false
}
})()
return recording
}
export async function stopVisualCapture(recording: VisualCapture | undefined) {
if (!recording) return []
recording.running = false
try {
await recording.capture
} finally {
await recording.session.detach().catch(() => undefined)
}
return recording.frames
}
@@ -0,0 +1,8 @@
export * from "./analyzer"
export * from "./capture"
export * from "./invariant"
export * from "./model"
export * from "./probe"
export * from "./regions"
export * from "./reporter"
export * from "./scenario"

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