@neurocode-ai/app 1.18.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (577) hide show
  1. package/AGENTS.md +35 -0
  2. package/README.md +50 -0
  3. package/V1_API_MIGRATION.md +220 -0
  4. package/bunfig.toml +3 -0
  5. package/create-effect-simplification-spec.md +515 -0
  6. package/e2e/performance/AGENTS.md +13 -0
  7. package/e2e/performance/README.md +79 -0
  8. package/e2e/performance/benchmark.ts +144 -0
  9. package/e2e/performance/chrome-trace.ts +95 -0
  10. package/e2e/performance/playwright.config.ts +20 -0
  11. package/e2e/performance/playwright.uncapped.config.ts +13 -0
  12. package/e2e/performance/timeline/first-navigation-benchmark.spec.ts +87 -0
  13. package/e2e/performance/timeline/first-navigation-metrics.ts +32 -0
  14. package/e2e/performance/timeline/first-navigation-probe.ts +86 -0
  15. package/e2e/performance/timeline/home-tab-navigation-benchmark.spec.ts +114 -0
  16. package/e2e/performance/timeline/navigation-milestones.ts +128 -0
  17. package/e2e/performance/timeline/review-pane-scaling-benchmark.spec.ts +312 -0
  18. package/e2e/performance/timeline/session-parent-hydration-benchmark.spec.ts +172 -0
  19. package/e2e/performance/timeline/session-tab-flash.spec.ts +67 -0
  20. package/e2e/performance/timeline/session-tab-repaint-probe.ts +251 -0
  21. package/e2e/performance/timeline/session-tab-switch-benchmark.spec.ts +144 -0
  22. package/e2e/performance/timeline/session-tab-switch-metrics.ts +59 -0
  23. package/e2e/performance/timeline/session-tab-switch-probe.ts +226 -0
  24. package/e2e/performance/timeline/session-timeline-benchmark.fixture.ts +504 -0
  25. package/e2e/performance/timeline/session-timeline-benchmark.spec.ts +306 -0
  26. package/e2e/performance/timeline/session-timeline-profile.ts +40 -0
  27. package/e2e/performance/timeline/session-timeline-stream-probe.ts +547 -0
  28. package/e2e/performance/timeline/session-timeline-stress.fixture.ts +369 -0
  29. package/e2e/performance/timeline/timeline-test-helpers.ts +134 -0
  30. package/e2e/performance/timeline-stability/README.md +61 -0
  31. package/e2e/performance/timeline-stability/adverse.spec.ts +250 -0
  32. package/e2e/performance/timeline-stability/context-matrix.spec.ts +192 -0
  33. package/e2e/performance/timeline-stability/environment-matrix.spec.ts +113 -0
  34. package/e2e/performance/timeline-stability/file-matrix.spec.ts +121 -0
  35. package/e2e/performance/timeline-stability/file-mutation.spec.ts +113 -0
  36. package/e2e/performance/timeline-stability/fixture.test.ts +68 -0
  37. package/e2e/performance/timeline-stability/fixture.ts +566 -0
  38. package/e2e/performance/timeline-stability/interaction.spec.ts +242 -0
  39. package/e2e/performance/timeline-stability/lifecycle.spec.ts +157 -0
  40. package/e2e/performance/timeline-stability/oracle-browser.spec.ts +75 -0
  41. package/e2e/performance/timeline-stability/playwright.config.ts +17 -0
  42. package/e2e/performance/timeline-stability/scroll-interaction.spec.ts +353 -0
  43. package/e2e/performance/timeline-stability/shell-matrix.spec.ts +255 -0
  44. package/e2e/performance/timeline-stability/tool-mutation.spec.ts +106 -0
  45. package/e2e/performance/timeline-stability/tools.spec.ts +198 -0
  46. package/e2e/performance/timeline-stability/transition-matrix.spec.ts +272 -0
  47. package/e2e/performance/unit/chrome-trace-write.test.ts +15 -0
  48. package/e2e/performance/unit/first-navigation-metrics.test.ts +32 -0
  49. package/e2e/performance/unit/mock-server.test.ts +46 -0
  50. package/e2e/performance/unit/navigation-milestones.test.ts +34 -0
  51. package/e2e/performance/unit/session-tab-repaint-probe.test.ts +42 -0
  52. package/e2e/performance/unit/session-tab-switch-metrics.test.ts +86 -0
  53. package/e2e/performance/unit/session-tab-switch-probe.test.ts +55 -0
  54. package/e2e/performance/unit/session-timeline-stream-probe.test.ts +14 -0
  55. package/e2e/performance/unit/session-timeline-visual-tracking.test.ts +16 -0
  56. package/e2e/performance/unit/timeline-test-helpers.test.ts +10 -0
  57. package/e2e/performance/unit/visual-stability.test.ts +392 -0
  58. package/e2e/regression/cross-server-tab-close.spec.ts +153 -0
  59. package/e2e/regression/file-browser-sidebar-tab-switch.spec.ts +150 -0
  60. package/e2e/regression/legacy-new-session.spec.ts +41 -0
  61. package/e2e/regression/new-session-panel-corner.spec.ts +83 -0
  62. package/e2e/regression/prompt-input-v2-command-draft.spec.ts +50 -0
  63. package/e2e/regression/prompt-thinking-level.spec.ts +84 -0
  64. package/e2e/regression/remote-session-settings.spec.ts +302 -0
  65. package/e2e/regression/remote-tab-busy.spec.ts +131 -0
  66. package/e2e/regression/review-image-flash.spec.ts +202 -0
  67. package/e2e/regression/review-line-comment.spec.ts +158 -0
  68. package/e2e/regression/review-open-file.spec.ts +163 -0
  69. package/e2e/regression/review-state-persistence.spec.ts +155 -0
  70. package/e2e/regression/review-tab-switch.spec.ts +147 -0
  71. package/e2e/regression/review-terminal-stacked.spec.ts +308 -0
  72. package/e2e/regression/session-list-path-loading.spec.ts +41 -0
  73. package/e2e/regression/session-request-docks.spec.ts +221 -0
  74. package/e2e/regression/session-timeline-accessibility.spec.ts +22 -0
  75. package/e2e/regression/session-timeline-collapse-state.spec.ts +439 -0
  76. package/e2e/regression/session-timeline-context-resize.spec.ts +374 -0
  77. package/e2e/regression/session-timeline-context-state.spec.ts +31 -0
  78. package/e2e/regression/session-timeline-file-projection.spec.ts +52 -0
  79. package/e2e/regression/session-timeline-file-state.spec.ts +94 -0
  80. package/e2e/regression/session-timeline-history-root.spec.ts +242 -0
  81. package/e2e/regression/session-timeline-lifecycle-state.spec.ts +111 -0
  82. package/e2e/regression/session-timeline-locale-projection.spec.ts +25 -0
  83. package/e2e/regression/session-timeline-projection.spec.ts +287 -0
  84. package/e2e/regression/session-timeline-reasoning-projection.spec.ts +93 -0
  85. package/e2e/regression/session-timeline-reducer-projection.spec.ts +43 -0
  86. package/e2e/regression/session-timeline-shell-outline.spec.ts +228 -0
  87. package/e2e/regression/session-timeline-tool-projection.spec.ts +99 -0
  88. package/e2e/regression/session-timeline-tool-state.spec.ts +77 -0
  89. package/e2e/regression/session-timeline-transport.spec.ts +116 -0
  90. package/e2e/regression/session-todo-dock-navigation.spec.ts +190 -0
  91. package/e2e/regression/subagent-child-navigation.spec.ts +203 -0
  92. package/e2e/regression/tab-navigate-mousedown.spec.ts +127 -0
  93. package/e2e/regression/terminal-composer-focus.spec.ts +227 -0
  94. package/e2e/regression/terminal-hidden.spec.ts +117 -0
  95. package/e2e/regression/terminal-tab-switch.spec.ts +165 -0
  96. package/e2e/reproduction/timeline-suspense/index.html +34 -0
  97. package/e2e/reproduction/timeline-suspense/main.tsx +315 -0
  98. package/e2e/reproduction/timeline-suspense/playwright.config.ts +34 -0
  99. package/e2e/reproduction/timeline-suspense/timeline-suspense.repro.ts +179 -0
  100. package/e2e/reproduction/timeline-suspense/vite.config.ts +7 -0
  101. package/e2e/smoke/session-timeline.fixture.ts +315 -0
  102. package/e2e/smoke/session-timeline.spec.ts +740 -0
  103. package/e2e/tsconfig.json +20 -0
  104. package/e2e/utils/errors.ts +18 -0
  105. package/e2e/utils/mock-server.ts +443 -0
  106. package/e2e/utils/sse-transport.ts +312 -0
  107. package/e2e/utils/visual-stability/analyzer.ts +209 -0
  108. package/e2e/utils/visual-stability/capture.ts +51 -0
  109. package/e2e/utils/visual-stability/index.ts +8 -0
  110. package/e2e/utils/visual-stability/invariant.ts +112 -0
  111. package/e2e/utils/visual-stability/model.ts +47 -0
  112. package/e2e/utils/visual-stability/probe.ts +226 -0
  113. package/e2e/utils/visual-stability/regions.ts +18 -0
  114. package/e2e/utils/visual-stability/reporter.ts +63 -0
  115. package/e2e/utils/visual-stability/scenario.ts +20 -0
  116. package/e2e/utils/visual-stability.ts +54 -0
  117. package/e2e/utils/waits.ts +11 -0
  118. package/happydom.ts +75 -0
  119. package/index.html +28 -0
  120. package/package.json +96 -0
  121. package/playwright.config.ts +45 -0
  122. package/public/_headers +17 -0
  123. package/public/apple-touch-icon-v3.png +1 -0
  124. package/public/apple-touch-icon.png +1 -0
  125. package/public/assets/Inter.ttf +0 -0
  126. package/public/assets/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
  127. package/public/favicon-96x96-v3.png +1 -0
  128. package/public/favicon-96x96.png +1 -0
  129. package/public/favicon-v3.ico +1 -0
  130. package/public/favicon-v3.svg +1 -0
  131. package/public/favicon.ico +1 -0
  132. package/public/favicon.svg +1 -0
  133. package/public/oc-theme-preload.js +40 -0
  134. package/public/site.webmanifest +1 -0
  135. package/public/social-share-zen.png +1 -0
  136. package/public/social-share.png +1 -0
  137. package/public/web-app-manifest-192x192.png +1 -0
  138. package/public/web-app-manifest-512x512.png +1 -0
  139. package/src/addons/serialize.test.ts +327 -0
  140. package/src/addons/serialize.ts +642 -0
  141. package/src/app.tsx +659 -0
  142. package/src/assets/help/home.png +0 -0
  143. package/src/assets/help/introducing-tabs.mp4 +0 -0
  144. package/src/assets/help/placeholder.png +0 -0
  145. package/src/assets/help/tabs.png +0 -0
  146. package/src/components/command-palette.ts +281 -0
  147. package/src/components/command-tooltip-keybind.test.ts +22 -0
  148. package/src/components/command-tooltip-keybind.ts +11 -0
  149. package/src/components/debug-bar.tsx +550 -0
  150. package/src/components/dialog-command-palette-v2.css +220 -0
  151. package/src/components/dialog-command-palette-v2.tsx +342 -0
  152. package/src/components/dialog-connect-provider.stories.tsx +73 -0
  153. package/src/components/dialog-connect-provider.tsx +1178 -0
  154. package/src/components/dialog-custom-provider-form.ts +158 -0
  155. package/src/components/dialog-custom-provider.test.ts +80 -0
  156. package/src/components/dialog-custom-provider.tsx +330 -0
  157. package/src/components/dialog-edit-project-v2.tsx +156 -0
  158. package/src/components/dialog-edit-project.tsx +170 -0
  159. package/src/components/dialog-fork.tsx +104 -0
  160. package/src/components/dialog-manage-models.tsx +265 -0
  161. package/src/components/dialog-release-notes.tsx +144 -0
  162. package/src/components/dialog-select-directory-v2.css +107 -0
  163. package/src/components/dialog-select-directory-v2.tsx +378 -0
  164. package/src/components/dialog-select-directory.tsx +191 -0
  165. package/src/components/dialog-select-file.tsx +189 -0
  166. package/src/components/dialog-select-mcp.tsx +92 -0
  167. package/src/components/dialog-select-model-search.test.ts +19 -0
  168. package/src/components/dialog-select-model-search.ts +18 -0
  169. package/src/components/dialog-select-model-unpaid-v2.stories.tsx +55 -0
  170. package/src/components/dialog-select-model-unpaid-v2.tsx +175 -0
  171. package/src/components/dialog-select-model-unpaid.tsx +147 -0
  172. package/src/components/dialog-select-model.tsx +549 -0
  173. package/src/components/dialog-select-server.tsx +722 -0
  174. package/src/components/dialog-settings.tsx +94 -0
  175. package/src/components/dialog-usage-exceeded.tsx +44 -0
  176. package/src/components/directory-picker-domain.test.ts +248 -0
  177. package/src/components/directory-picker-domain.ts +407 -0
  178. package/src/components/directory-picker-policy.ts +7 -0
  179. package/src/components/directory-picker.test.ts +21 -0
  180. package/src/components/directory-picker.tsx +45 -0
  181. package/src/components/edit-project.ts +122 -0
  182. package/src/components/file-tree-v2-model.test.ts +74 -0
  183. package/src/components/file-tree-v2-model.ts +107 -0
  184. package/src/components/file-tree-v2.tsx +299 -0
  185. package/src/components/file-tree.test.ts +80 -0
  186. package/src/components/file-tree.tsx +509 -0
  187. package/src/components/help-button.tsx +148 -0
  188. package/src/components/link.tsx +26 -0
  189. package/src/components/model-tooltip.tsx +124 -0
  190. package/src/components/pierre-tree.test.ts +23 -0
  191. package/src/components/prompt-input/attachments.test.ts +108 -0
  192. package/src/components/prompt-input/attachments.ts +228 -0
  193. package/src/components/prompt-input/build-request-parts.test.ts +396 -0
  194. package/src/components/prompt-input/build-request-parts.ts +213 -0
  195. package/src/components/prompt-input/context-items.tsx +98 -0
  196. package/src/components/prompt-input/contracts.ts +57 -0
  197. package/src/components/prompt-input/drag-overlay.tsx +25 -0
  198. package/src/components/prompt-input/editor-dom.test.ts +99 -0
  199. package/src/components/prompt-input/editor-dom.ts +148 -0
  200. package/src/components/prompt-input/files.ts +98 -0
  201. package/src/components/prompt-input/history-store.ts +47 -0
  202. package/src/components/prompt-input/history.test.ts +153 -0
  203. package/src/components/prompt-input/history.ts +256 -0
  204. package/src/components/prompt-input/image-attachments.css +43 -0
  205. package/src/components/prompt-input/image-attachments.tsx +168 -0
  206. package/src/components/prompt-input/paste.ts +24 -0
  207. package/src/components/prompt-input/placeholder.test.ts +48 -0
  208. package/src/components/prompt-input/placeholder.ts +20 -0
  209. package/src/components/prompt-input/slash-popover.tsx +376 -0
  210. package/src/components/prompt-input/submission-state.ts +33 -0
  211. package/src/components/prompt-input/submit.test.ts +598 -0
  212. package/src/components/prompt-input/submit.ts +634 -0
  213. package/src/components/prompt-input/transient-state.ts +46 -0
  214. package/src/components/prompt-input-v2.tsx +592 -0
  215. package/src/components/prompt-input.stories.tsx +223 -0
  216. package/src/components/prompt-input.tsx +1790 -0
  217. package/src/components/prompt-project-selector.tsx +593 -0
  218. package/src/components/prompt-workspace-selector.tsx +128 -0
  219. package/src/components/server/server-row-menu.tsx +96 -0
  220. package/src/components/server/server-row.tsx +127 -0
  221. package/src/components/session/index.ts +7 -0
  222. package/src/components/session/open-in-app-v2.tsx +98 -0
  223. package/src/components/session/open-in-app.tsx +229 -0
  224. package/src/components/session/session-context-breakdown.test.ts +61 -0
  225. package/src/components/session/session-context-breakdown.ts +132 -0
  226. package/src/components/session/session-context-format.ts +20 -0
  227. package/src/components/session/session-context-metrics.test.ts +99 -0
  228. package/src/components/session/session-context-metrics.ts +65 -0
  229. package/src/components/session/session-context-tab.tsx +343 -0
  230. package/src/components/session/session-header.tsx +568 -0
  231. package/src/components/session/session-new-design-view.tsx +16 -0
  232. package/src/components/session/session-new-view.tsx +91 -0
  233. package/src/components/session/session-sortable-tab-v2.tsx +74 -0
  234. package/src/components/session/session-sortable-tab.tsx +78 -0
  235. package/src/components/session/session-sortable-terminal-tab-v2.tsx +284 -0
  236. package/src/components/session/session-sortable-terminal-tab.tsx +193 -0
  237. package/src/components/session-context-usage.tsx +169 -0
  238. package/src/components/settings-dialog.tsx +43 -0
  239. package/src/components/settings-general.tsx +794 -0
  240. package/src/components/settings-keybinds.tsx +553 -0
  241. package/src/components/settings-list.tsx +5 -0
  242. package/src/components/settings-models.tsx +149 -0
  243. package/src/components/settings-providers.tsx +264 -0
  244. package/src/components/settings-server-picker.tsx +106 -0
  245. package/src/components/settings-servers.tsx +33 -0
  246. package/src/components/settings-v2/dialog-server-v2.tsx +134 -0
  247. package/src/components/settings-v2/dialog-settings-v2.tsx +98 -0
  248. package/src/components/settings-v2/general.tsx +722 -0
  249. package/src/components/settings-v2/index.tsx +1 -0
  250. package/src/components/settings-v2/interface-transition.stories.tsx +76 -0
  251. package/src/components/settings-v2/interface-transition.tsx +54 -0
  252. package/src/components/settings-v2/models.tsx +138 -0
  253. package/src/components/settings-v2/parts/list.tsx +6 -0
  254. package/src/components/settings-v2/parts/row.tsx +20 -0
  255. package/src/components/settings-v2/providers.tsx +264 -0
  256. package/src/components/settings-v2/servers.tsx +139 -0
  257. package/src/components/settings-v2/settings-v2.css +675 -0
  258. package/src/components/status-popover-body.tsx +514 -0
  259. package/src/components/status-popover-indicator.test.ts +36 -0
  260. package/src/components/status-popover-indicator.ts +20 -0
  261. package/src/components/status-popover.tsx +190 -0
  262. package/src/components/terminal.tsx +759 -0
  263. package/src/components/titlebar-history.test.ts +63 -0
  264. package/src/components/titlebar-history.ts +57 -0
  265. package/src/components/titlebar-session-events.test.ts +32 -0
  266. package/src/components/titlebar-session-events.ts +35 -0
  267. package/src/components/titlebar-tab-gesture.test.ts +33 -0
  268. package/src/components/titlebar-tab-gesture.ts +17 -0
  269. package/src/components/titlebar-tab-nav.css +100 -0
  270. package/src/components/titlebar-tab-nav.tsx +429 -0
  271. package/src/components/titlebar-tab-popover.css +127 -0
  272. package/src/components/titlebar-tab-popover.tsx +92 -0
  273. package/src/components/titlebar-tab-strip.tsx +333 -0
  274. package/src/components/titlebar.css +77 -0
  275. package/src/components/titlebar.tsx +776 -0
  276. package/src/components/ui/drawer.tsx +119 -0
  277. package/src/components/updater-action.test.ts +26 -0
  278. package/src/components/updater-action.ts +51 -0
  279. package/src/components/virtual-scroll-element.test.ts +18 -0
  280. package/src/components/virtual-scroll-element.ts +4 -0
  281. package/src/components/windows-app-menu.tsx +131 -0
  282. package/src/constants/file-picker.ts +89 -0
  283. package/src/context/closed-tabs.ts +40 -0
  284. package/src/context/command-keybind.test.ts +69 -0
  285. package/src/context/command.test.ts +55 -0
  286. package/src/context/command.tsx +467 -0
  287. package/src/context/comments.test.ts +188 -0
  288. package/src/context/comments.tsx +261 -0
  289. package/src/context/directory-sync.ts +155 -0
  290. package/src/context/file/content-cache.ts +88 -0
  291. package/src/context/file/path.test.ts +360 -0
  292. package/src/context/file/path.ts +151 -0
  293. package/src/context/file/tree-store.ts +174 -0
  294. package/src/context/file/types.ts +41 -0
  295. package/src/context/file/view-cache.ts +147 -0
  296. package/src/context/file/watcher.test.ts +149 -0
  297. package/src/context/file/watcher.ts +53 -0
  298. package/src/context/file-content-eviction-accounting.test.ts +65 -0
  299. package/src/context/file.tsx +303 -0
  300. package/src/context/global-sync/bootstrap.test.ts +231 -0
  301. package/src/context/global-sync/bootstrap.ts +536 -0
  302. package/src/context/global-sync/child-store.test.ts +276 -0
  303. package/src/context/global-sync/child-store.ts +396 -0
  304. package/src/context/global-sync/event-reducer.test.ts +616 -0
  305. package/src/context/global-sync/event-reducer.ts +478 -0
  306. package/src/context/global-sync/eviction.ts +28 -0
  307. package/src/context/global-sync/home-session-index.test.ts +154 -0
  308. package/src/context/global-sync/home-session-index.ts +174 -0
  309. package/src/context/global-sync/mcp.test.ts +54 -0
  310. package/src/context/global-sync/mcp.ts +19 -0
  311. package/src/context/global-sync/queue.test.ts +46 -0
  312. package/src/context/global-sync/queue.ts +87 -0
  313. package/src/context/global-sync/session-cache.test.ts +104 -0
  314. package/src/context/global-sync/session-cache.ts +62 -0
  315. package/src/context/global-sync/session-load.ts +33 -0
  316. package/src/context/global-sync/session-trim.test.ts +59 -0
  317. package/src/context/global-sync/session-trim.ts +57 -0
  318. package/src/context/global-sync/types.ts +135 -0
  319. package/src/context/global-sync/utils.test.ts +133 -0
  320. package/src/context/global-sync/utils.ts +171 -0
  321. package/src/context/global.tsx +161 -0
  322. package/src/context/highlights.tsx +233 -0
  323. package/src/context/language.tsx +243 -0
  324. package/src/context/layout-helpers.ts +38 -0
  325. package/src/context/layout-scroll.test.ts +64 -0
  326. package/src/context/layout-scroll.ts +126 -0
  327. package/src/context/layout-tabs.test.ts +82 -0
  328. package/src/context/layout-tabs.ts +103 -0
  329. package/src/context/layout.test.ts +69 -0
  330. package/src/context/layout.tsx +1075 -0
  331. package/src/context/local-agent.test.ts +29 -0
  332. package/src/context/local-agent.ts +7 -0
  333. package/src/context/local.tsx +417 -0
  334. package/src/context/mcp.ts +19 -0
  335. package/src/context/model-variant.test.ts +86 -0
  336. package/src/context/model-variant.ts +52 -0
  337. package/src/context/models.tsx +173 -0
  338. package/src/context/notification.tsx +478 -0
  339. package/src/context/permission-auto-respond.test.ts +125 -0
  340. package/src/context/permission-auto-respond.ts +60 -0
  341. package/src/context/permission.tsx +484 -0
  342. package/src/context/platform.tsx +142 -0
  343. package/src/context/prompt-state.test.ts +29 -0
  344. package/src/context/prompt-state.ts +266 -0
  345. package/src/context/prompt.tsx +170 -0
  346. package/src/context/sdk.tsx +17 -0
  347. package/src/context/server-sdk.test.ts +196 -0
  348. package/src/context/server-sdk.tsx +444 -0
  349. package/src/context/server-session-v2-reducer.test.ts +156 -0
  350. package/src/context/server-session-v2-reducer.ts +502 -0
  351. package/src/context/server-session.test.ts +1638 -0
  352. package/src/context/server-session.ts +1425 -0
  353. package/src/context/server-sync.test.ts +231 -0
  354. package/src/context/server-sync.tsx +753 -0
  355. package/src/context/server.test.ts +245 -0
  356. package/src/context/server.tsx +360 -0
  357. package/src/context/settings.test.ts +79 -0
  358. package/src/context/settings.tsx +531 -0
  359. package/src/context/sync-optimistic.test.ts +123 -0
  360. package/src/context/sync.tsx +119 -0
  361. package/src/context/tab-memory.ts +36 -0
  362. package/src/context/tabs.test.ts +107 -0
  363. package/src/context/tabs.tsx +387 -0
  364. package/src/context/terminal-title.ts +24 -0
  365. package/src/context/terminal.test.ts +91 -0
  366. package/src/context/terminal.tsx +546 -0
  367. package/src/custom-elements.d.ts +1 -0
  368. package/src/desktop-menu.test.ts +13 -0
  369. package/src/desktop-menu.ts +223 -0
  370. package/src/entry.tsx +182 -0
  371. package/src/env.d.ts +31 -0
  372. package/src/hooks/provider-catalog.test.ts +59 -0
  373. package/src/hooks/provider-catalog.ts +27 -0
  374. package/src/hooks/use-providers.ts +71 -0
  375. package/src/i18n/ar.ts +1008 -0
  376. package/src/i18n/br.ts +1024 -0
  377. package/src/i18n/bs.ts +1100 -0
  378. package/src/i18n/da.ts +1092 -0
  379. package/src/i18n/de.ts +1037 -0
  380. package/src/i18n/en.ts +1102 -0
  381. package/src/i18n/es.ts +1108 -0
  382. package/src/i18n/fr.ts +1037 -0
  383. package/src/i18n/ja.ts +1016 -0
  384. package/src/i18n/ko.ts +1015 -0
  385. package/src/i18n/no.ts +1111 -0
  386. package/src/i18n/parity.test.ts +118 -0
  387. package/src/i18n/pl.ts +1024 -0
  388. package/src/i18n/ru.ts +1103 -0
  389. package/src/i18n/th.ts +1087 -0
  390. package/src/i18n/tr.ts +1106 -0
  391. package/src/i18n/uk.ts +1105 -0
  392. package/src/i18n/zh.ts +1078 -0
  393. package/src/i18n/zht.ts +1074 -0
  394. package/src/index.css +330 -0
  395. package/src/index.ts +30 -0
  396. package/src/pages/directory-layout.tsx +122 -0
  397. package/src/pages/error-description.test.ts +17 -0
  398. package/src/pages/error-description.ts +11 -0
  399. package/src/pages/error.tsx +372 -0
  400. package/src/pages/home/home-controller.ts +108 -0
  401. package/src/pages/home/home-projects-controller.tsx +128 -0
  402. package/src/pages/home/home-projects-view.tsx +615 -0
  403. package/src/pages/home/home-projects.tsx +40 -0
  404. package/src/pages/home/home-scroll-controller.ts +145 -0
  405. package/src/pages/home/home-session-search-controller.ts +114 -0
  406. package/src/pages/home/home-sessions-controller.tsx +314 -0
  407. package/src/pages/home/home-sessions-view.tsx +550 -0
  408. package/src/pages/home/home-sessions.tsx +48 -0
  409. package/src/pages/home/legacy-home.tsx +142 -0
  410. package/src/pages/home-session-archive.test.ts +51 -0
  411. package/src/pages/home-session-archive.ts +27 -0
  412. package/src/pages/home-session-open.test.ts +31 -0
  413. package/src/pages/home-session-open.ts +14 -0
  414. package/src/pages/home.tsx +50 -0
  415. package/src/pages/layout/deep-links.ts +50 -0
  416. package/src/pages/layout/helpers.test.ts +321 -0
  417. package/src/pages/layout/helpers.ts +150 -0
  418. package/src/pages/layout/inline-editor.tsx +126 -0
  419. package/src/pages/layout/project-avatar-state.ts +49 -0
  420. package/src/pages/layout/session-tab-avatar.tsx +60 -0
  421. package/src/pages/layout/sidebar-items.tsx +335 -0
  422. package/src/pages/layout/sidebar-project.tsx +377 -0
  423. package/src/pages/layout/sidebar-shell.tsx +125 -0
  424. package/src/pages/layout/sidebar-workspace.tsx +488 -0
  425. package/src/pages/layout-new.tsx +53 -0
  426. package/src/pages/layout.tsx +2441 -0
  427. package/src/pages/new-session.tsx +282 -0
  428. package/src/pages/session/composer/index.ts +4 -0
  429. package/src/pages/session/composer/prompt-model-selection.ts +133 -0
  430. package/src/pages/session/composer/session-composer-controls.ts +129 -0
  431. package/src/pages/session/composer/session-composer-region-controller.ts +145 -0
  432. package/src/pages/session/composer/session-composer-region.tsx +168 -0
  433. package/src/pages/session/composer/session-composer-state.test.ts +138 -0
  434. package/src/pages/session/composer/session-composer-state.ts +204 -0
  435. package/src/pages/session/composer/session-followup-dock.tsx +109 -0
  436. package/src/pages/session/composer/session-permission-dock.tsx +74 -0
  437. package/src/pages/session/composer/session-question-dock.tsx +640 -0
  438. package/src/pages/session/composer/session-request-tree.ts +52 -0
  439. package/src/pages/session/composer/session-revert-dock.stories.tsx +124 -0
  440. package/src/pages/session/composer/session-revert-dock.tsx +188 -0
  441. package/src/pages/session/composer/session-todo-dock.tsx +277 -0
  442. package/src/pages/session/composer/todo-panel-motion.stories.tsx +622 -0
  443. package/src/pages/session/file-tab-scroll.test.ts +40 -0
  444. package/src/pages/session/file-tab-scroll.ts +67 -0
  445. package/src/pages/session/file-tabs.tsx +800 -0
  446. package/src/pages/session/handoff.ts +36 -0
  447. package/src/pages/session/helpers.test.ts +214 -0
  448. package/src/pages/session/helpers.ts +209 -0
  449. package/src/pages/session/message-gesture.test.ts +62 -0
  450. package/src/pages/session/message-gesture.ts +21 -0
  451. package/src/pages/session/message-id-from-hash.ts +6 -0
  452. package/src/pages/session/new-session-layout.ts +2 -0
  453. package/src/pages/session/review-tab.tsx +172 -0
  454. package/src/pages/session/session-layout.ts +30 -0
  455. package/src/pages/session/session-lineage.ts +69 -0
  456. package/src/pages/session/session-model-helpers.test.ts +151 -0
  457. package/src/pages/session/session-model-helpers.ts +63 -0
  458. package/src/pages/session/session-ownership.ts +37 -0
  459. package/src/pages/session/session-panel-layout.test.ts +19 -0
  460. package/src/pages/session/session-panel-layout.ts +6 -0
  461. package/src/pages/session/session-panel-width.test.ts +52 -0
  462. package/src/pages/session/session-panel-width.ts +19 -0
  463. package/src/pages/session/session-side-panel.tsx +865 -0
  464. package/src/pages/session/terminal-label.ts +16 -0
  465. package/src/pages/session/terminal-panel-v2.tsx +352 -0
  466. package/src/pages/session/terminal-panel.test.ts +25 -0
  467. package/src/pages/session/terminal-panel.tsx +343 -0
  468. package/src/pages/session/timeline/measure.test.ts +30 -0
  469. package/src/pages/session/timeline/measure.ts +5 -0
  470. package/src/pages/session/timeline/message-timeline.tsx +1864 -0
  471. package/src/pages/session/timeline/model.test.ts +95 -0
  472. package/src/pages/session/timeline/model.ts +128 -0
  473. package/src/pages/session/timeline/observe-element-offset.test.ts +199 -0
  474. package/src/pages/session/timeline/observe-element-offset.ts +73 -0
  475. package/src/pages/session/timeline/projection.test.ts +96 -0
  476. package/src/pages/session/timeline/projection.ts +81 -0
  477. package/src/pages/session/timeline/row-reconciliation.ts +56 -0
  478. package/src/pages/session/timeline/rows-current.test.ts +170 -0
  479. package/src/pages/session/timeline/rows.ts +344 -0
  480. package/src/pages/session/timeline/summary-diffs.test.ts +42 -0
  481. package/src/pages/session/timeline/summary-diffs.ts +20 -0
  482. package/src/pages/session/timeline/timeline-row.ts +80 -0
  483. package/src/pages/session/timeline/virtual-items.ts +3 -0
  484. package/src/pages/session/usage-exceeded-dialogs.tsx +104 -0
  485. package/src/pages/session/use-composer-commands.tsx +83 -0
  486. package/src/pages/session/use-session-commands.tsx +606 -0
  487. package/src/pages/session/use-session-hash-scroll.test.ts +16 -0
  488. package/src/pages/session/use-session-hash-scroll.ts +202 -0
  489. package/src/pages/session/v2/review-diff-kinds.test.ts +70 -0
  490. package/src/pages/session/v2/review-diff-kinds.ts +62 -0
  491. package/src/pages/session/v2/review-panel-v2-state.ts +41 -0
  492. package/src/pages/session/v2/review-panel-v2.tsx +255 -0
  493. package/src/pages/session/v2/session-file-browser-tab.tsx +181 -0
  494. package/src/pages/session/v2/session-file-list-v2.tsx +166 -0
  495. package/src/pages/session.tsx +2385 -0
  496. package/src/sst-env.d.ts +12 -0
  497. package/src/theme-preload.test.ts +46 -0
  498. package/src/updater.ts +17 -0
  499. package/src/utils/agent.ts +44 -0
  500. package/src/utils/aim.ts +138 -0
  501. package/src/utils/base64.ts +10 -0
  502. package/src/utils/comment-note.ts +88 -0
  503. package/src/utils/diffs.test.ts +75 -0
  504. package/src/utils/diffs.ts +50 -0
  505. package/src/utils/file-manager.ts +24 -0
  506. package/src/utils/id.ts +93 -0
  507. package/src/utils/notification-click.test.ts +27 -0
  508. package/src/utils/notification-click.ts +13 -0
  509. package/src/utils/path-key.ts +24 -0
  510. package/src/utils/persist.test.ts +211 -0
  511. package/src/utils/persist.ts +661 -0
  512. package/src/utils/prompt.test.ts +44 -0
  513. package/src/utils/prompt.ts +203 -0
  514. package/src/utils/refcount.test.ts +49 -0
  515. package/src/utils/refcount.ts +32 -0
  516. package/src/utils/runtime-adapters.test.ts +64 -0
  517. package/src/utils/runtime-adapters.ts +39 -0
  518. package/src/utils/same.ts +6 -0
  519. package/src/utils/scoped-cache.test.ts +69 -0
  520. package/src/utils/scoped-cache.ts +104 -0
  521. package/src/utils/search-keydown.ts +116 -0
  522. package/src/utils/server-compat.test.ts +193 -0
  523. package/src/utils/server-compat.ts +512 -0
  524. package/src/utils/server-errors.test.ts +175 -0
  525. package/src/utils/server-errors.ts +109 -0
  526. package/src/utils/server-health.test.ts +178 -0
  527. package/src/utils/server-health.ts +172 -0
  528. package/src/utils/server-protocol.test.ts +40 -0
  529. package/src/utils/server-protocol.ts +35 -0
  530. package/src/utils/server-scope.test.ts +61 -0
  531. package/src/utils/server-scope.ts +73 -0
  532. package/src/utils/server.test.ts +23 -0
  533. package/src/utils/server.ts +62 -0
  534. package/src/utils/session-message.test.ts +214 -0
  535. package/src/utils/session-message.ts +355 -0
  536. package/src/utils/session-route.test.ts +71 -0
  537. package/src/utils/session-route.ts +39 -0
  538. package/src/utils/session-title.ts +7 -0
  539. package/src/utils/session.test.ts +94 -0
  540. package/src/utils/session.ts +37 -0
  541. package/src/utils/solid-dnd.tsx +49 -0
  542. package/src/utils/sound.ts +102 -0
  543. package/src/utils/terminal-websocket-url.test.ts +80 -0
  544. package/src/utils/terminal-websocket-url.ts +35 -0
  545. package/src/utils/terminal-writer.test.ts +64 -0
  546. package/src/utils/terminal-writer.ts +65 -0
  547. package/src/utils/time.ts +22 -0
  548. package/src/utils/toast.tsx +34 -0
  549. package/src/utils/uuid.test.ts +78 -0
  550. package/src/utils/uuid.ts +12 -0
  551. package/src/utils/worktree.test.ts +58 -0
  552. package/src/utils/worktree.ts +76 -0
  553. package/src/wsl/add-server-probes.ts +57 -0
  554. package/src/wsl/context.tsx +36 -0
  555. package/src/wsl/dialog-add-server.tsx +463 -0
  556. package/src/wsl/dialog-add-wsl-server.css +392 -0
  557. package/src/wsl/settings-model.test.ts +231 -0
  558. package/src/wsl/settings-model.ts +341 -0
  559. package/src/wsl/settings.tsx +173 -0
  560. package/src/wsl/types.ts +85 -0
  561. package/sst-env.d.ts +10 -0
  562. package/test-browser/command-palette.test.ts +78 -0
  563. package/test-browser/motion-spring.test.ts +20 -0
  564. package/test-browser/prompt-attachments.test.ts +90 -0
  565. package/test-browser/prompt-persistence.test.ts +71 -0
  566. package/test-browser/prompt-scope.test.ts +14 -0
  567. package/test-browser/prompt-submission-state.test.ts +73 -0
  568. package/test-browser/prompt-transient-state.test.ts +38 -0
  569. package/test-browser/review-panel-v2-state.test.ts +65 -0
  570. package/test-browser/session-lineage.test.ts +231 -0
  571. package/test-browser/session-ownership.test.ts +45 -0
  572. package/test-browser/solid-virtual.test.ts +112 -0
  573. package/test-browser/toast-owner.test.ts +26 -0
  574. package/tsconfig.json +26 -0
  575. package/vendor/opencode-ai-client-1.17.13.tgz +0 -0
  576. package/vite.config.ts +33 -0
  577. package/vite.js +48 -0
@@ -0,0 +1,93 @@
1
+ import { expect, test } from "@playwright/test"
2
+ import {
3
+ assistantMessage,
4
+ reasoningPart,
5
+ setupTimeline,
6
+ status,
7
+ textPart,
8
+ toolPart,
9
+ userMessage,
10
+ } from "../performance/timeline-stability/fixture"
11
+
12
+ const profiles = [
13
+ { name: "summaries off no reasoning", summaries: false, reasoning: "", other: false, thinking: true, body: false },
14
+ {
15
+ name: "summaries off reasoning heading",
16
+ summaries: false,
17
+ reasoning: "## Inspecting stability",
18
+ other: false,
19
+ thinking: true,
20
+ body: false,
21
+ },
22
+ {
23
+ name: "summaries off with visible tool",
24
+ summaries: false,
25
+ reasoning: "## Inspecting stability",
26
+ other: true,
27
+ thinking: true,
28
+ body: false,
29
+ },
30
+ { name: "summaries on no content", summaries: true, reasoning: "", other: false, thinking: true, body: false },
31
+ {
32
+ name: "summaries on blank reasoning",
33
+ summaries: true,
34
+ reasoning: " ",
35
+ other: false,
36
+ thinking: true,
37
+ body: false,
38
+ },
39
+ {
40
+ name: "summaries on visible reasoning",
41
+ summaries: true,
42
+ reasoning: "## Inspecting stability",
43
+ other: false,
44
+ thinking: false,
45
+ body: true,
46
+ },
47
+ {
48
+ name: "summaries on visible tool no reasoning",
49
+ summaries: true,
50
+ reasoning: "",
51
+ other: true,
52
+ thinking: false,
53
+ body: false,
54
+ },
55
+ ] as const
56
+
57
+ for (const profile of profiles) {
58
+ test(`projects busy reasoning profile ${profile.name}`, async ({ page }) => {
59
+ const reasoningID = `prt_reasoning_matrix_${profiles.indexOf(profile)}`
60
+ const parts = [
61
+ ...(profile.reasoning ? [reasoningPart(reasoningID, profile.reasoning)] : []),
62
+ ...(profile.other
63
+ ? [toolPart(`prt_reasoning_tool_${profiles.indexOf(profile)}`, "skill", "running", { name: "inspect" })]
64
+ : []),
65
+ ]
66
+ const timeline = await setupTimeline(page, {
67
+ messages: [userMessage(), assistantMessage(parts, { completed: false })],
68
+ settings: { showReasoningSummaries: profile.summaries },
69
+ })
70
+ await timeline.send(status("busy"), 150)
71
+
72
+ await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(profile.thinking ? 1 : 0)
73
+ await expect(page.locator(`[data-timeline-part-id="${reasoningID}"]`)).toHaveCount(profile.body ? 1 : 0)
74
+ if (!profile.summaries && profile.reasoning.trim()) {
75
+ await expect(page.getByText("Inspecting stability", { exact: true })).toBeVisible()
76
+ }
77
+ })
78
+ }
79
+
80
+ test("does not infer reasoning visibility from provider identity", async ({ page }) => {
81
+ const timeline = await setupTimeline(page, {
82
+ messages: [
83
+ userMessage(),
84
+ assistantMessage([textPart("prt_provider_text", "No reasoning payload")], { completed: false }),
85
+ ],
86
+ settings: { showReasoningSummaries: true },
87
+ })
88
+ await timeline.send(status("busy"), 150)
89
+
90
+ await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0)
91
+ await expect(page.locator('[data-timeline-part-id*="reasoning"]')).toHaveCount(0)
92
+ await expect(page.locator('[data-timeline-part-id="prt_provider_text"]')).toBeVisible()
93
+ })
@@ -0,0 +1,43 @@
1
+ import { expect, test } from "@playwright/test"
2
+ import {
3
+ assistantMessage,
4
+ completedAssistantInfo,
5
+ messageUpdated,
6
+ partUpdated,
7
+ setupTimeline,
8
+ shell,
9
+ status,
10
+ textPart,
11
+ toolPart,
12
+ userMessage,
13
+ } from "../performance/timeline-stability/fixture"
14
+
15
+ test("groups singleton and separated context operations at correct boundaries", async ({ page }) => {
16
+ const parts = [
17
+ toolPart("prt_boundary_01_read", "read", "completed", { filePath: "src/a.ts" }),
18
+ textPart("prt_boundary_02_text", "Boundary text"),
19
+ toolPart("prt_boundary_03_glob", "glob", "completed", { path: ".", pattern: "**/*.ts" }),
20
+ toolPart("prt_boundary_04_grep", "grep", "completed", { path: ".", pattern: "stable" }),
21
+ shell("prt_boundary_05_shell", "completed", "done"),
22
+ toolPart("prt_boundary_06_list", "list", "completed", { path: "src" }),
23
+ ]
24
+ await setupTimeline(page, { messages: [userMessage(), assistantMessage(parts)] })
25
+
26
+ await expect(page.locator('[data-timeline-part-ids="prt_boundary_01_read"]')).toBeVisible()
27
+ await expect(page.locator('[data-timeline-part-ids="prt_boundary_03_glob,prt_boundary_04_grep"]')).toBeVisible()
28
+ await expect(page.locator('[data-timeline-part-ids="prt_boundary_06_list"]')).toBeVisible()
29
+ await expect(page.locator('[data-timeline-row="AssistantPart"]')).toHaveCount(5)
30
+ })
31
+
32
+ test("reducer-hardening: converges when idle arrives before final part and message completion", async ({ page }) => {
33
+ const textID = "prt_event_order_text"
34
+ const assistant = assistantMessage([textPart(textID, "Partial")], { completed: false })
35
+ const timeline = await setupTimeline(page, { messages: [userMessage(), assistant] })
36
+ await timeline.send(status("busy"), 100)
37
+ await timeline.send(status("idle"), 100)
38
+ await timeline.send(partUpdated(textPart(textID, "Final after early idle")), 120)
39
+ await timeline.send(messageUpdated(completedAssistantInfo(assistant.info)), 250)
40
+
41
+ await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0)
42
+ await expect(page.locator(`[data-timeline-part-id="${textID}"]`)).toContainText("Final after early idle")
43
+ })
@@ -0,0 +1,228 @@
1
+ import { expect, test, type Locator, type Page } from "@playwright/test"
2
+ import {
3
+ assistantMessage,
4
+ setupTimeline,
5
+ shell,
6
+ textPart,
7
+ toolPart,
8
+ userMessage,
9
+ } from "../performance/timeline-stability/fixture"
10
+
11
+ for (const deviceScaleFactor of [1.25, 1.5]) {
12
+ test(`keeps the shell outline inside a fractionally short virtual row at ${deviceScaleFactor}x`, async ({ page }) => {
13
+ const shellID = "prt_shell_outline"
14
+ const timeline = await setupTimeline(page, {
15
+ messages: [userMessage(), assistantMessage([shell(shellID, "completed", "shell output")])],
16
+ settings: { newLayoutDesigns: true, shellToolPartsExpanded: true },
17
+ reducedMotion: true,
18
+ deviceScaleFactor,
19
+ })
20
+ const part = page.locator(`[data-timeline-part-id="${shellID}"]`)
21
+ const output = part.locator('[data-component="bash-output"]')
22
+ const row = page.locator("[data-timeline-key]", { has: part })
23
+ await expect(output).toBeVisible()
24
+ await timeline.settle()
25
+
26
+ const geometry = await row.evaluate((element) => {
27
+ const output = element.querySelector<HTMLElement>('[data-component="bash-output"]')
28
+ if (!output) throw new Error("Shell output is unavailable")
29
+ const rowRect = element.getBoundingClientRect()
30
+ const outputRect = output.getBoundingClientRect()
31
+ // Match a rounded-down measurement at a fractional device-pixel phase.
32
+ element.style.height = `${outputRect.bottom - rowRect.top - 0.49}px`
33
+ element.style.transform = "translateY(0.25px)"
34
+ output.style.setProperty("--v2-border-border-base", "rgb(255, 0, 255)")
35
+ output.style.setProperty("background", "rgb(0, 0, 0)", "important")
36
+ const style = getComputedStyle(output)
37
+ return {
38
+ outputWidth: outputRect.width,
39
+ outputHeight: outputRect.height,
40
+ borderColor: style.borderTopColor,
41
+ boxShadow: style.boxShadow,
42
+ clipMargin: getComputedStyle(element).overflowClipMargin,
43
+ }
44
+ })
45
+ await timeline.settle()
46
+
47
+ const clipped = await row.evaluate((element) => {
48
+ const output = element.querySelector<HTMLElement>('[data-component="bash-output"]')!
49
+ return output.getBoundingClientRect().bottom - element.getBoundingClientRect().bottom
50
+ })
51
+ expect(clipped).toBeCloseTo(0.49, 1)
52
+
53
+ expect(await page.evaluate(() => devicePixelRatio)).toBe(deviceScaleFactor)
54
+ const edges = await captureCardEdges(page, output)
55
+
56
+ expect(edges.box.width).toBeCloseTo(geometry.outputWidth, 2)
57
+ expect(edges.box.height).toBeCloseTo(geometry.outputHeight, 2)
58
+ expect(geometry.borderColor).toBe("rgb(255, 0, 255)")
59
+ expect(geometry.boxShadow).toBe("none")
60
+ expect(geometry.clipMargin).toBe("0.5px")
61
+ expect(edges.magenta.top).toBeGreaterThan(0.75)
62
+ expect(edges.magenta.bottom).toBeGreaterThan(0.75)
63
+ expect(edges.magenta.vertical).toBeGreaterThanOrEqual(2)
64
+ })
65
+ }
66
+
67
+ test("keeps the patch card inside a fractionally short virtual row", async ({ page }) => {
68
+ const patchID = "prt_patch_outline"
69
+ const file = {
70
+ filePath: "src/outline.ts",
71
+ relativePath: "src/outline.ts",
72
+ type: "update",
73
+ additions: 1,
74
+ deletions: 1,
75
+ before: "const outline = false\n",
76
+ after: "const outline = true\n",
77
+ }
78
+ const timeline = await setupTimeline(page, {
79
+ messages: [
80
+ userMessage(),
81
+ assistantMessage([
82
+ toolPart(patchID, "apply_patch", "completed", { files: [file.filePath] }, { metadata: { files: [file] } }),
83
+ ]),
84
+ ],
85
+ settings: { editToolPartsExpanded: true, newLayoutDesigns: true },
86
+ reducedMotion: true,
87
+ })
88
+ const part = page.locator(`[data-timeline-part-id="${patchID}"]`)
89
+ const card = part.locator('[data-component="accordion"][data-scope="apply-patch"]')
90
+ const row = page.locator("[data-timeline-key]", { has: part })
91
+ await expect(card).toBeVisible()
92
+ await timeline.settle()
93
+
94
+ const geometry = await row.evaluate((element) => {
95
+ const card = element.querySelector<HTMLElement>('[data-component="accordion"][data-scope="apply-patch"]')
96
+ if (!card) throw new Error("Patch card is unavailable")
97
+ const rowRect = element.getBoundingClientRect()
98
+ const cardRect = card.getBoundingClientRect()
99
+ element.style.height = `${cardRect.bottom - rowRect.top - 0.49}px`
100
+ const clipMargin = getComputedStyle(element).overflowClipMargin
101
+ const bottom = element.getBoundingClientRect().bottom
102
+ return {
103
+ overflow: card.getBoundingClientRect().bottom - bottom,
104
+ paintOverflow: card.getBoundingClientRect().bottom - bottom - Number.parseFloat(clipMargin),
105
+ clipMargin,
106
+ cardWidth: cardRect.width,
107
+ cardHeight: cardRect.height,
108
+ }
109
+ })
110
+ await timeline.settle()
111
+
112
+ expect(geometry.overflow).toBeCloseTo(0.49, 1)
113
+ expect(geometry.paintOverflow).toBeLessThanOrEqual(0)
114
+ const edges = await captureCardEdges(page, card)
115
+ expect(edges.box.width).toBeCloseTo(geometry.cardWidth, 2)
116
+ expect(edges.box.height).toBeCloseTo(geometry.cardHeight, 2)
117
+ expect(edges.luminance.top).toBeLessThan(245)
118
+ expect(edges.luminance.bottom).toBeLessThan(245)
119
+ expect(Math.abs(edges.luminance.bottom - edges.luminance.top)).toBeLessThan(10)
120
+ expect(geometry.clipMargin).toBe("0.5px")
121
+ })
122
+
123
+ test("allows paint rounding for every framed row but not fixed turn gaps", async ({ page }) => {
124
+ const secondUserID = "msg_outline_second_user"
125
+ await setupTimeline(page, {
126
+ messages: [
127
+ userMessage(undefined, {
128
+ summary: {
129
+ diffs: [
130
+ {
131
+ file: "src/summary.ts",
132
+ additions: 1,
133
+ deletions: 1,
134
+ patch: "@@ -1 +1 @@\n-export const value = 1\n+export const value = 2",
135
+ },
136
+ ],
137
+ },
138
+ }),
139
+ assistantMessage([textPart("prt_outline_text", "Assistant text")]),
140
+ userMessage(undefined, { id: secondUserID, created: 1700000010000 }),
141
+ assistantMessage([], {
142
+ id: "msg_outline_second_assistant",
143
+ parentID: secondUserID,
144
+ created: 1700000011000,
145
+ }),
146
+ ],
147
+ })
148
+ await expect(page.locator('[data-timeline-row="DiffSummary"]')).toBeVisible()
149
+ await expect(page.locator('[data-timeline-row="TurnGap"]')).toBeVisible()
150
+
151
+ const rows = await page.locator("[data-timeline-key]").evaluateAll((elements) =>
152
+ elements.map((element) => ({
153
+ tag: element.querySelector<HTMLElement>("[data-timeline-row]")?.dataset.timelineRow,
154
+ clipMargin: getComputedStyle(element).overflowClipMargin,
155
+ })),
156
+ )
157
+ expect(rows.filter((row) => row.tag !== "TurnGap").every((row) => row.clipMargin === "0.5px")).toBe(true)
158
+ expect(rows.filter((row) => row.tag === "TurnGap")).toEqual([{ tag: "TurnGap", clipMargin: "0px" }])
159
+ })
160
+
161
+ async function captureCardEdges(page: Page, card: Locator) {
162
+ const box = await card.boundingBox()
163
+ if (!box) throw new Error("Tool card bounds are unavailable")
164
+ const viewport = page.viewportSize()
165
+ if (!viewport) throw new Error("Viewport bounds are unavailable")
166
+ const screenshot = await page.screenshot()
167
+ return page.evaluate(
168
+ async ({ source, box, viewport }) => {
169
+ const image = new Image()
170
+ image.src = source
171
+ await image.decode()
172
+ const canvas = document.createElement("canvas")
173
+ canvas.width = image.naturalWidth
174
+ canvas.height = image.naturalHeight
175
+ const context = canvas.getContext("2d")
176
+ if (!context) throw new Error("2D canvas is unavailable")
177
+ context.drawImage(image, 0, 0)
178
+ const scale = {
179
+ x: image.naturalWidth / viewport.width,
180
+ y: image.naturalHeight / viewport.height,
181
+ }
182
+ const rows = (candidates: number[]) => {
183
+ const left = Math.floor((box.x + 8) * scale.x)
184
+ const width = Math.floor((box.width - 16) * scale.x)
185
+ return candidates.map((row) => {
186
+ const pixels = context.getImageData(left, row, width, 1).data
187
+ const indexes = Array.from({ length: width }, (_, index) => index * 4)
188
+ return {
189
+ luminance:
190
+ indexes
191
+ .map((index) => (pixels[index]! + pixels[index + 1]! + pixels[index + 2]!) / 3)
192
+ .reduce((sum, value) => sum + value, 0) / width,
193
+ magenta:
194
+ indexes.filter((index) => pixels[index]! > 200 && pixels[index + 1]! < 180 && pixels[index + 2]! > 200)
195
+ .length / width,
196
+ }
197
+ })
198
+ }
199
+ const pixels = context.getImageData(0, 0, image.naturalWidth, image.naturalHeight).data
200
+ const columns = new Uint32Array(image.naturalWidth)
201
+ for (let index = 0; index < pixels.length; index += 4) {
202
+ if (pixels[index]! <= 200 || pixels[index + 1]! >= 180 || pixels[index + 2]! <= 200) continue
203
+ columns[(index / 4) % image.naturalWidth] = columns[(index / 4) % image.naturalWidth]! + 1
204
+ }
205
+ const top = box.y * scale.y
206
+ const bottom = (box.y + box.height) * scale.y
207
+ const topRows = rows([Math.floor(top) - 1, Math.floor(top), Math.ceil(top)])
208
+ const bottomRows = rows([Math.floor(bottom) - 2, Math.floor(bottom) - 1, Math.ceil(bottom) - 1])
209
+ return {
210
+ box,
211
+ luminance: {
212
+ top: Math.min(...topRows.map((row) => row.luminance)),
213
+ bottom: rows([Math.ceil(bottom) - 1])[0]!.luminance,
214
+ },
215
+ magenta: {
216
+ top: Math.max(...topRows.map((row) => row.magenta)),
217
+ bottom: Math.max(...bottomRows.map((row) => row.magenta)),
218
+ vertical: Array.from(columns).filter((count) => count > box.height * scale.y * 0.75).length,
219
+ },
220
+ }
221
+ },
222
+ {
223
+ source: `data:image/png;base64,${screenshot.toString("base64")}`,
224
+ viewport,
225
+ box,
226
+ },
227
+ )
228
+ }
@@ -0,0 +1,99 @@
1
+ import { expect, test } from "@playwright/test"
2
+ import {
3
+ assistantMessage,
4
+ partUpdated,
5
+ setupTimeline,
6
+ toolPart,
7
+ userMessage,
8
+ } from "../performance/timeline-stability/fixture"
9
+
10
+ test("renders every tool error outcome without leaking hidden tools", async ({ page }) => {
11
+ const ordinary = ["bash", "edit", "write", "apply_patch", "webfetch", "websearch", "task", "skill", "mcp_probe"]
12
+ const parts = ordinary.map((tool, index) =>
13
+ toolPart(`prt_error_${index}`, tool, "error", errorInput(tool), { error: `${tool} failed visibly` }),
14
+ )
15
+ parts.push(
16
+ toolPart("prt_question_dismissed", "question", "error", questionInput(), {
17
+ error: "The user dismissed this question",
18
+ }),
19
+ toolPart("prt_question_error", "question", "error", questionInput(), { error: "Question transport failed" }),
20
+ toolPart("prt_todo_error", "todowrite", "error", { todos: [] }, { error: "Hidden todo failure" }),
21
+ )
22
+ await setupTimeline(page, { messages: [userMessage(), assistantMessage(parts)] })
23
+
24
+ await expect(page.locator('[data-kind="tool-error-card"]')).toHaveCount(ordinary.length + 1)
25
+ await expect(page.getByText(/dismissed/i)).toBeVisible()
26
+ await expect(page.locator('[data-timeline-part-id="prt_todo_error"]')).toHaveCount(0)
27
+ for (let index = 0; index < ordinary.length; index++) {
28
+ await expect(page.locator(`[data-timeline-part-id="prt_error_${index}"]`)).toBeVisible()
29
+ }
30
+ })
31
+
32
+ test("transitions shell and question through running error outcomes", async ({ page }) => {
33
+ const shellID = "prt_transition_error_shell"
34
+ const questionID = "prt_transition_error_question"
35
+ const timeline = await setupTimeline(page, {
36
+ messages: [
37
+ userMessage(),
38
+ assistantMessage(
39
+ [
40
+ toolPart(shellID, "bash", "pending", { command: "exit 1" }),
41
+ toolPart(questionID, "question", "pending", questionInput()),
42
+ ],
43
+ { completed: false },
44
+ ),
45
+ ],
46
+ })
47
+ await timeline.waitForPart(shellID)
48
+ await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toHaveCount(0)
49
+ await timeline.send(partUpdated(toolPart(shellID, "bash", "running", { command: "exit 1" })), 120)
50
+ await timeline.send(partUpdated(toolPart(questionID, "question", "running", questionInput())), 180)
51
+ await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toHaveCount(0)
52
+ await timeline.send(
53
+ partUpdated(toolPart(shellID, "bash", "error", { command: "exit 1" }, { error: "Command exited 1" })),
54
+ 180,
55
+ )
56
+ await timeline.send(
57
+ partUpdated(
58
+ toolPart(questionID, "question", "error", questionInput(), { error: "The user dismissed this question" }),
59
+ ),
60
+ 250,
61
+ )
62
+
63
+ await expect(page.locator(`[data-timeline-part-id="${shellID}"] [data-kind="tool-error-card"]`)).toBeVisible()
64
+ await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toContainText(/dismissed/i)
65
+ })
66
+
67
+ test("labels all web search provider variants", async ({ page }) => {
68
+ const parts = [
69
+ toolPart(
70
+ "prt_search_parallel",
71
+ "websearch",
72
+ "completed",
73
+ { query: "parallel" },
74
+ { metadata: { provider: "parallel" } },
75
+ ),
76
+ toolPart("prt_search_exa", "websearch", "completed", { query: "exa" }, { metadata: { provider: "exa" } }),
77
+ toolPart("prt_search_generic", "websearch", "completed", { query: "generic" }),
78
+ ]
79
+ await setupTimeline(page, { messages: [userMessage(), assistantMessage(parts)] })
80
+
81
+ await expect(page.getByRole("button", { name: /Parallel Web Search/ })).toBeVisible()
82
+ await expect(page.getByRole("button", { name: /Exa Web Search/ })).toBeVisible()
83
+ await expect(page.getByRole("button", { name: /^Web Search/ })).toBeVisible()
84
+ })
85
+
86
+ function questionInput() {
87
+ return { questions: [{ header: "Stability", question: "Keep it stable?", options: [] }] }
88
+ }
89
+
90
+ function errorInput(tool: string) {
91
+ if (tool === "bash") return { command: "exit 1" }
92
+ if (["edit", "write"].includes(tool)) return { filePath: "src/error.ts", content: "" }
93
+ if (tool === "apply_patch") return { files: ["src/error.ts"] }
94
+ if (tool === "webfetch") return { url: "https://example.com" }
95
+ if (tool === "websearch") return { query: "failure" }
96
+ if (tool === "task") return { description: "Fail task", subagent_type: "explore" }
97
+ if (tool === "skill") return { name: "failure" }
98
+ return { target: "failure" }
99
+ }
@@ -0,0 +1,77 @@
1
+ import { expect, test } from "@playwright/test"
2
+ import {
3
+ assistantMessage,
4
+ partUpdated,
5
+ setupTimeline,
6
+ toolPart,
7
+ userMessage,
8
+ } from "../performance/timeline-stability/fixture"
9
+
10
+ test("updates expanded web search links without resetting expansion", async ({ page }) => {
11
+ const searchID = "prt_websearch_mutation"
12
+ const input = { query: "timeline stability" }
13
+ const timeline = await setupTimeline(page, {
14
+ messages: [
15
+ userMessage(),
16
+ assistantMessage([toolPart(searchID, "websearch", "completed", input, { output: "https://example.com/one" })]),
17
+ ],
18
+ })
19
+ const wrapper = page.locator(`[data-timeline-part-id="${searchID}"]`)
20
+ const trigger = wrapper.locator('[data-slot="collapsible-trigger"]')
21
+ await trigger.click()
22
+ await expect(trigger).toHaveAttribute("aria-expanded", "true")
23
+ await timeline.send(
24
+ partUpdated(
25
+ toolPart(searchID, "websearch", "completed", input, {
26
+ output: "https://example.com/one\nhttps://example.com/two",
27
+ }),
28
+ ),
29
+ 300,
30
+ )
31
+ await expect(trigger).toHaveAttribute("aria-expanded", "true")
32
+ await expect(wrapper.locator('a[href="https://example.com/two"]')).toBeVisible()
33
+ })
34
+
35
+ test("preserves an expanded tool error card across duplicate delivery", async ({ page }) => {
36
+ const toolID = "prt_duplicate_error"
37
+ const failed = toolPart(toolID, "bash", "error", { command: "exit 1" }, { error: "Command failed visibly" })
38
+ const timeline = await setupTimeline(page, { messages: [userMessage(), assistantMessage([failed])] })
39
+ const wrapper = page.locator(`[data-timeline-part-id="${toolID}"]`)
40
+ const trigger = wrapper.locator('[data-slot="collapsible-trigger"]')
41
+ await trigger.click()
42
+ await expect(trigger).toHaveAttribute("aria-expanded", "true")
43
+ await timeline.send(partUpdated(failed), 150)
44
+ await timeline.send(partUpdated(failed), 250)
45
+ await expect(trigger).toHaveAttribute("aria-expanded", "true")
46
+ await expect(wrapper).toContainText("Command failed visibly")
47
+ })
48
+
49
+ test("renders multiple question answers and preserves open state on answer updates", async ({ page }) => {
50
+ const questionID = "prt_multi_question"
51
+ const input = {
52
+ questions: [
53
+ { header: "First", question: "First choice?", options: [] },
54
+ { header: "Second", question: "Second choice?", options: [], multiple: true },
55
+ ],
56
+ }
57
+ const timeline = await setupTimeline(page, {
58
+ messages: [
59
+ userMessage(),
60
+ assistantMessage([
61
+ toolPart(questionID, "question", "completed", input, { metadata: { answers: [["A"], ["B", "C"]] } }),
62
+ ]),
63
+ ],
64
+ })
65
+ const wrapper = page.locator(`[data-timeline-part-id="${questionID}"]`)
66
+ const trigger = wrapper.locator('[data-slot="collapsible-trigger"]')
67
+ await expect(trigger).toHaveAttribute("aria-expanded", "true")
68
+ await timeline.send(
69
+ partUpdated(
70
+ toolPart(questionID, "question", "completed", input, { metadata: { answers: [["Updated"], ["B", "C"]] } }),
71
+ ),
72
+ 300,
73
+ )
74
+ await expect(trigger).toHaveAttribute("aria-expanded", "true")
75
+ await expect(wrapper).toContainText("Updated")
76
+ await expect(wrapper).toContainText("B, C")
77
+ })
@@ -0,0 +1,116 @@
1
+ import { expect, test } from "@playwright/test"
2
+ import {
3
+ assistantMessage,
4
+ partUpdated,
5
+ setupTimeline,
6
+ status,
7
+ textPart,
8
+ userMessage,
9
+ } from "../performance/timeline-stability/fixture"
10
+
11
+ test("keeps one connection open while delivering multiple events", async ({ page }) => {
12
+ const timeline = await setupTimeline(page)
13
+
14
+ const first = await timeline.transport.send(partUpdated(textPart("prt_transport_first", "first event")))
15
+ const second = await timeline.transport.send(partUpdated(textPart("prt_transport_second", "second event")))
16
+
17
+ await timeline.waitForPart("prt_transport_first")
18
+ await timeline.waitForPart("prt_transport_second")
19
+ expect(first.connectionID).toBe(second.connectionID)
20
+ expect(await timeline.transport.connections()).toHaveLength(1)
21
+ expect(await timeline.transport.acknowledgements()).toHaveLength(2)
22
+ })
23
+
24
+ test("delivers a burst from one stream chunk", async ({ page }) => {
25
+ const timeline = await setupTimeline(page)
26
+ const acknowledgements = await timeline.transport.burst([
27
+ partUpdated(textPart("prt_transport_burst_a", "burst a")),
28
+ partUpdated(textPart("prt_transport_burst_b", "burst b")),
29
+ ])
30
+
31
+ await timeline.waitForPart("prt_transport_burst_a")
32
+ await timeline.waitForPart("prt_transport_burst_b")
33
+ expect(acknowledgements.map((item) => item.chunkCount)).toEqual([1, 1])
34
+ expect(new Set(acknowledgements.map((item) => item.deliveryID)).size).toBe(2)
35
+ })
36
+
37
+ test("parses split JSON and a split multibyte code point", async ({ page }) => {
38
+ const timeline = await setupTimeline(page)
39
+ const payload = partUpdated(textPart("prt_transport_split", "split snowman \u2603\u2603\u2603"))
40
+ const encoded = new TextEncoder().encode(`data: ${JSON.stringify(payload)}\n\n`)
41
+ const snowman = new TextEncoder().encode("\u2603")[0]!
42
+ const multibyte = encoded.indexOf(snowman)
43
+
44
+ const acknowledgement = await timeline.transport.split(payload, [9, multibyte + 1, multibyte + 2])
45
+
46
+ await timeline.waitForPart("prt_transport_split")
47
+ await expect(page.locator('[data-timeline-part-id="prt_transport_split"]')).toContainText(
48
+ "split snowman \u2603\u2603\u2603",
49
+ )
50
+ expect(acknowledgement.chunkCount).toBe(4)
51
+ })
52
+
53
+ test("delivers server heartbeat without mutating the timeline", async ({ page }) => {
54
+ const timeline = await setupTimeline(page, {
55
+ messages: [userMessage(), assistantMessage([textPart("prt_transport_steady", "steady")])],
56
+ })
57
+ const before = await page.locator("[data-timeline-row]").allTextContents()
58
+
59
+ await timeline.transport.heartbeat()
60
+ await timeline.settle()
61
+
62
+ expect(await page.locator("[data-timeline-row]").allTextContents()).toEqual(before)
63
+ expect(await timeline.transport.connections()).toHaveLength(1)
64
+ })
65
+
66
+ test("reconnects after a clean close", async ({ page }) => {
67
+ const timeline = await setupTimeline(page, { eventRetry: 10 })
68
+ const first = await timeline.transport.waitForConnection()
69
+
70
+ await timeline.transport.close()
71
+ const second = await timeline.transport.waitForConnection({ after: first.id })
72
+ await timeline.transport.send(partUpdated(textPart("prt_transport_close", "after close")))
73
+
74
+ await timeline.waitForPart("prt_transport_close")
75
+ expect(second.id).toBeGreaterThan(first.id)
76
+ expect((await timeline.transport.connections())[0]?.endedBy).toBe("close")
77
+ })
78
+
79
+ test("reconnects after a stream error", async ({ page }) => {
80
+ const timeline = await setupTimeline(page, { eventRetry: 10 })
81
+ const first = await timeline.transport.waitForConnection()
82
+
83
+ await timeline.transport.error("contract failure")
84
+ const second = await timeline.transport.waitForConnection({ after: first.id })
85
+ await timeline.transport.send(status("busy"))
86
+
87
+ await expect.poll(async () => (await timeline.transport.connections()).length).toBe(2)
88
+ expect(second.id).toBeGreaterThan(first.id)
89
+ expect((await timeline.transport.connections())[0]?.endedBy).toBe("error")
90
+ })
91
+
92
+ test("does not request replay when reconnecting the volatile V2 event stream", async ({ page }) => {
93
+ const timeline = await setupTimeline(page, { eventRetry: 10, protocol: "v2" })
94
+ const first = await timeline.transport.send(partUpdated(textPart("prt_transport_id", "event with id")), {
95
+ id: "timeline-event-7",
96
+ })
97
+ await timeline.waitForPart("prt_transport_id")
98
+
99
+ await timeline.transport.error("retry with event id")
100
+ const connection = await timeline.transport.waitForConnection({ after: first.connectionID })
101
+
102
+ expect(first.eventID).toBe("timeline-event-7")
103
+ expect(connection.headers["last-event-id"]).toBeUndefined()
104
+ })
105
+
106
+ test("passes through non-event fetches", async ({ page }) => {
107
+ const timeline = await setupTimeline(page)
108
+
109
+ const health = await page.evaluate(async () => {
110
+ const response = await fetch("/global/health")
111
+ return response.json()
112
+ })
113
+
114
+ expect(health).toEqual({ healthy: true })
115
+ expect(await timeline.transport.connections()).toHaveLength(1)
116
+ })