@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,1864 @@
1
+ import {
2
+ createEffect,
3
+ createMemo,
4
+ createSignal,
5
+ For,
6
+ Index,
7
+ on,
8
+ onCleanup,
9
+ onMount,
10
+ Show,
11
+ type Accessor,
12
+ type JSX,
13
+ } from "solid-js"
14
+ import { createStore, produce } from "solid-js/store"
15
+ import { Dynamic } from "solid-js/web"
16
+ import { useNavigate } from "@solidjs/router"
17
+ import { useMutation } from "@tanstack/solid-query"
18
+ import { createVirtualizer, defaultRangeExtractor, elementScroll, type VirtualItem } from "@tanstack/solid-virtual"
19
+ import { Accordion } from "@neurocode-ai/ui/accordion"
20
+ import { Button } from "@neurocode-ai/ui/button"
21
+ import { Card } from "@neurocode-ai/ui/card"
22
+ import {
23
+ ContextToolGroup,
24
+ Message,
25
+ MessageDivider,
26
+ Part as MessagePart,
27
+ partDefaultOpen,
28
+ type UserActions,
29
+ } from "@neurocode-ai/session-ui/message-part"
30
+ import { DiffChanges } from "@neurocode-ai/ui/diff-changes"
31
+ import { FileIcon } from "@neurocode-ai/ui/file-icon"
32
+ import { Icon } from "@neurocode-ai/ui/icon"
33
+ import { IconButton } from "@neurocode-ai/ui/icon-button"
34
+ import { Icon as IconV2 } from "@neurocode-ai/ui/v2/icon"
35
+ import { IconButtonV2 } from "@neurocode-ai/ui/v2/icon-button-v2"
36
+ import { DropdownMenu } from "@neurocode-ai/ui/dropdown-menu"
37
+ import { MenuV2 } from "@neurocode-ai/ui/v2/menu-v2"
38
+ import { Dialog } from "@neurocode-ai/ui/dialog"
39
+ import { DialogFooter, DialogHeader, DialogTitleGroup, DialogV2 } from "@neurocode-ai/ui/v2/dialog-v2"
40
+ import { InlineInput } from "@neurocode-ai/ui/inline-input"
41
+ import { ButtonV2 } from "@neurocode-ai/ui/v2/button-v2"
42
+ import { SessionRetry } from "@neurocode-ai/session-ui/session-retry"
43
+ import { isScrollKeyTarget, scrollKey, scrollKeyOwner, ScrollView } from "@neurocode-ai/ui/scroll-view"
44
+ import { StickyAccordionHeader } from "@neurocode-ai/ui/sticky-accordion-header"
45
+ import { TextField } from "@neurocode-ai/ui/text-field"
46
+ import { TextReveal } from "@neurocode-ai/ui/text-reveal"
47
+ import { TextShimmer } from "@neurocode-ai/ui/text-shimmer"
48
+ import type {
49
+ AssistantMessage,
50
+ Message as MessageType,
51
+ Part as PartType,
52
+ ToolPart,
53
+ UserMessage,
54
+ } from "@neurocode-ai/sdk/v2"
55
+ import { showToast } from "@/utils/toast"
56
+ import { getDirectory, getFilename } from "@neurocode-ai/core/util/path"
57
+ import { Popover as KobaltePopover } from "@kobalte/core/popover"
58
+ import { normalize } from "@neurocode-ai/session-ui/session-diff"
59
+ import { useFileComponent } from "@neurocode-ai/ui/context/file"
60
+ import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture"
61
+ import { SessionContextUsage } from "@/components/session-context-usage"
62
+ import { useDialog } from "@neurocode-ai/ui/context/dialog"
63
+ import { useLanguage } from "@/context/language"
64
+ import { useSessionKey } from "@/pages/session/session-layout"
65
+ import { useServerSDK } from "@/context/server-sdk"
66
+ import { usePlatform } from "@/context/platform"
67
+ import { useSettings } from "@/context/settings"
68
+ import { useTabs } from "@/context/tabs"
69
+ import { legacySessionHref, requireServerKey, sessionHref } from "@/utils/session-route"
70
+ import { useSDK } from "@/context/sdk"
71
+ import { useSync } from "@/context/sync"
72
+ import { notifySessionTabsRemoved } from "@/components/titlebar-session-events"
73
+ import { sessionTitle } from "@/utils/session-title"
74
+ import { scheduleConnectedMeasure } from "./measure"
75
+ import { observeElementOffsetReconnectAware } from "./observe-element-offset"
76
+ import { createTimelineProjection } from "./projection"
77
+ import { MessageComment, SummaryDiff, TimelineRow, TimelineRowMap } from "./rows"
78
+ import { filterVirtualIndexes } from "./virtual-items"
79
+
80
+ const emptyMessages: MessageType[] = []
81
+ const emptyParts: PartType[] = []
82
+ const emptyTools: ToolPart[] = []
83
+ const emptyAssistantMessages: AssistantMessage[] = []
84
+ const idle = { type: "idle" as const }
85
+
86
+ type FramedTimelineRow = Exclude<TimelineRow.TimelineRow, { _tag: "TurnGap" }>
87
+ type TimelineRowByTag<T extends TimelineRow.TimelineRow["_tag"]> = Extract<TimelineRow.TimelineRow, { _tag: T }>
88
+
89
+ const timelineFallbackItemSize = 60
90
+ const timelineCache = new Map<string, { measurements: VirtualItem[]; toolOpen: Record<string, boolean | undefined> }>()
91
+
92
+ const taskDescription = (part: PartType, sessionID: string) => {
93
+ if (part.type !== "tool" || part.tool !== "task") return
94
+ const metadata = "metadata" in part.state ? part.state.metadata : undefined
95
+ if (metadata?.sessionId !== sessionID) return
96
+ const value = part.state.input?.description
97
+ if (typeof value === "string" && value) return value
98
+ }
99
+
100
+ const boundaryTarget = (root: HTMLElement, target: EventTarget | null) => {
101
+ const current = target instanceof Element ? target : undefined
102
+ const nested = current?.closest("[data-scrollable]")
103
+ if (!nested || nested === root) return root
104
+ if (!(nested instanceof HTMLElement)) return root
105
+ return nested
106
+ }
107
+
108
+ const markBoundaryGesture = (input: {
109
+ root: HTMLDivElement
110
+ target: EventTarget | null
111
+ delta: number
112
+ onMarkScrollGesture: (target?: EventTarget | null) => void
113
+ }) => {
114
+ const target = boundaryTarget(input.root, input.target)
115
+ if (target === input.root) {
116
+ input.onMarkScrollGesture(input.root)
117
+ return
118
+ }
119
+ if (
120
+ shouldMarkBoundaryGesture({
121
+ delta: input.delta,
122
+ scrollTop: target.scrollTop,
123
+ scrollHeight: target.scrollHeight,
124
+ clientHeight: target.clientHeight,
125
+ })
126
+ ) {
127
+ input.onMarkScrollGesture(input.root)
128
+ }
129
+ }
130
+
131
+ function TimelineThinkingRow(props: { reasoningHeading?: string; showReasoningSummaries: boolean }) {
132
+ const language = useLanguage()
133
+
134
+ return (
135
+ <div data-slot="session-turn-thinking">
136
+ <TextShimmer text={language.t("ui.sessionTurn.status.thinking")} />
137
+ <Show when={!props.showReasoningSummaries}>
138
+ <TextReveal text={props.reasoningHeading} class="session-turn-thinking-heading" travel={25} duration={700} />
139
+ </Show>
140
+ </div>
141
+ )
142
+ }
143
+
144
+ function TimelineDiffSummaryRow(props: { diffs: SummaryDiff[] }) {
145
+ const language = useLanguage()
146
+ const maxFiles = 10
147
+ const [state, setState] = createStore({
148
+ showAll: false,
149
+ expanded: [] as string[],
150
+ })
151
+ const showAll = () => state.showAll
152
+ const expanded = () => state.expanded
153
+ const overflow = createMemo(() => Math.max(0, props.diffs.length - maxFiles))
154
+ const visible = createMemo(() => (showAll() ? props.diffs : props.diffs.slice(0, maxFiles)))
155
+
156
+ return (
157
+ <div
158
+ data-slot="session-turn-diffs"
159
+ data-component="session-turn-diffs-group"
160
+ data-show-all={showAll() || undefined}
161
+ >
162
+ <div data-slot="session-turn-diffs-header">
163
+ <span data-slot="session-turn-diffs-label">
164
+ {language.t(
165
+ props.diffs.length === 1 ? "ui.sessionTurn.diffs.changed.one" : "ui.sessionTurn.diffs.changed.other",
166
+ { count: String(props.diffs.length) },
167
+ )}
168
+ </span>
169
+ <DiffChanges changes={props.diffs} />
170
+ <Show when={overflow() > 0}>
171
+ <span data-slot="session-turn-diffs-toggle" onClick={() => setState("showAll", !showAll())}>
172
+ {showAll() ? language.t("ui.sessionTurn.diffs.showLess") : language.t("ui.sessionTurn.diffs.showAll")}
173
+ </span>
174
+ </Show>
175
+ </div>
176
+ <div data-component="session-turn-diffs-content">
177
+ <Accordion
178
+ multiple
179
+ style={{ "--sticky-accordion-offset": "44px" }}
180
+ value={expanded()}
181
+ onChange={(value) => setState("expanded", Array.isArray(value) ? value : value ? [value] : [])}
182
+ >
183
+ <For each={visible()}>
184
+ {(diff) => {
185
+ const opened = createMemo(() => expanded().includes(diff.file))
186
+
187
+ return (
188
+ <Accordion.Item value={diff.file}>
189
+ <StickyAccordionHeader>
190
+ <Accordion.Trigger>
191
+ <div data-slot="session-turn-diff-trigger">
192
+ <span data-slot="session-turn-diff-path">
193
+ <Show when={diff.file.includes("/")}>
194
+ <span data-slot="session-turn-diff-directory">{`\u202A${getDirectory(diff.file)}\u202C`}</span>
195
+ </Show>
196
+ <span data-slot="session-turn-diff-filename">{getFilename(diff.file)}</span>
197
+ </span>
198
+ <div data-slot="session-turn-diff-meta">
199
+ <span data-slot="session-turn-diff-changes">
200
+ <DiffChanges changes={diff} />
201
+ </span>
202
+ <span data-slot="session-turn-diff-chevron">
203
+ <Icon name="chevron-down" size="small" />
204
+ </span>
205
+ </div>
206
+ </div>
207
+ </Accordion.Trigger>
208
+ </StickyAccordionHeader>
209
+ <Accordion.Content>
210
+ <Show when={opened()}>
211
+ <TimelineDiffView diff={diff} />
212
+ </Show>
213
+ </Accordion.Content>
214
+ </Accordion.Item>
215
+ )
216
+ }}
217
+ </For>
218
+ </Accordion>
219
+ <Show when={!showAll() && overflow() > 0}>
220
+ <div data-slot="session-turn-diffs-more" onClick={() => setState("showAll", true)}>
221
+ {language.t("ui.sessionTurn.diffs.more", { count: String(overflow()) })}
222
+ </div>
223
+ </Show>
224
+ </div>
225
+ </div>
226
+ )
227
+ }
228
+
229
+ function TimelineDiffView(props: { diff: SummaryDiff }) {
230
+ const fileComponent = useFileComponent()
231
+ const view = normalize(props.diff)
232
+
233
+ return (
234
+ <div data-slot="session-turn-diff-view" data-scrollable>
235
+ <Dynamic component={fileComponent} mode="diff" virtualize={false} fileDiff={view.fileDiff} />
236
+ </div>
237
+ )
238
+ }
239
+
240
+ export function MessageTimeline(props: {
241
+ actions?: UserActions
242
+ scroll: { overflow: boolean; bottom: boolean; jump: boolean }
243
+ onResumeScroll: () => void
244
+ setScrollRef: (el: HTMLDivElement | undefined) => void
245
+ onScheduleScrollState: (el: HTMLDivElement) => void
246
+ onAutoScrollHandleScroll: () => void
247
+ onMarkScrollGesture: (target?: EventTarget | null) => void
248
+ hasScrollGesture: () => boolean
249
+ onUserScroll: () => void
250
+ onHistoryScroll: () => void
251
+ onAutoScrollInteraction: (event: MouseEvent) => void
252
+ shouldAnchorBottom: () => boolean
253
+ centered: boolean
254
+ setContentRef: (el: HTMLDivElement) => void
255
+ userMessages: UserMessage[]
256
+ anchor: (id: string) => string
257
+ setRevealMessage?: (fn: (id: string) => void) => void
258
+ setScrollToEnd?: (fn: () => void) => void
259
+ setHistoryAnchor?: (handlers: { capture: () => void; restore: (done: boolean) => void }) => void
260
+ }) {
261
+ let touchGesture: number | undefined
262
+
263
+ const navigate = useNavigate()
264
+ const serverSDK = useServerSDK()
265
+ const sdk = useSDK()
266
+ const sync = useSync()
267
+ const settings = useSettings()
268
+ const tabs = useTabs()
269
+ const dialog = useDialog()
270
+ const language = useLanguage()
271
+ const { params, sessionKey } = useSessionKey()
272
+ const ownerSessionKey = sessionKey()
273
+ const cached = timelineCache.get(ownerSessionKey)
274
+ const initialMeasurements = cached?.measurements
275
+ const coldBottomMount = !initialMeasurements?.length && props.shouldAnchorBottom()
276
+ const platform = usePlatform()
277
+
278
+ const [listRoot, setListRoot] = createSignal<HTMLDivElement>()
279
+ const sessionID = createMemo(() => params.id)
280
+ const sessionStatus = createMemo(() => {
281
+ const id = sessionID()
282
+ if (!id) return idle
283
+ return sync().data.session_status[id] ?? idle
284
+ })
285
+ const sessionMessages = createMemo(() => (sessionID() ? (sync().data.message[sessionID()!] ?? []) : []))
286
+ const projectedMessages = createMemo(() => {
287
+ const id = sessionID()
288
+ if (!id) return []
289
+ const visible = new Set(props.userMessages.map((message) => message.id))
290
+ const boundary = sessionMessages().find((message) => message.role === "user" && !visible.has(message.id))?.id
291
+ const messages = sync().data.session_message[id] ?? []
292
+ return boundary ? messages.filter((message) => message.id < boundary) : messages
293
+ })
294
+ const info = createMemo(() => {
295
+ const id = sessionID()
296
+ if (!id) return
297
+ return sync().session.get(id)
298
+ })
299
+ const titleValue = createMemo(() => info()?.title)
300
+ const titleLabel = createMemo(() => sessionTitle(titleValue()))
301
+ const shareUrl = createMemo(() => info()?.share?.url)
302
+ const shareEnabled = createMemo(() => sync().data.config.share !== "disabled")
303
+ const parentID = createMemo(() => info()?.parentID)
304
+ const parent = createMemo(() => {
305
+ const id = parentID()
306
+ if (!id) return
307
+ return sync().session.get(id)
308
+ })
309
+ const parentMessages = createMemo(() => {
310
+ const id = parentID()
311
+ if (!id) return emptyMessages
312
+ return sync().data.message[id] ?? emptyMessages
313
+ })
314
+ const parentTitle = createMemo(() => sessionTitle(parent()?.title) ?? language.t("command.session.new"))
315
+ const getMsgParts = (msgId: string) => sync().data.part[msgId] ?? emptyParts
316
+ const getMsgPart = (messageID: string, partID: string) => getMsgParts(messageID).find((part) => part.id === partID)
317
+ const childTaskDescription = createMemo(() => {
318
+ const id = sessionID()
319
+ if (!id) return
320
+ return parentMessages()
321
+ .flatMap((message) => getMsgParts(message.id))
322
+ .map((part) => taskDescription(part, id))
323
+ .findLast((value): value is string => !!value)
324
+ })
325
+ const childTitle = createMemo(() => {
326
+ if (!parentID()) return titleLabel() ?? ""
327
+ if (childTaskDescription()) return childTaskDescription()
328
+ const value = titleLabel()?.replace(/\s+\(@[^)]+ subagent\)$/, "")
329
+ if (value) return value
330
+ return language.t("command.session.new")
331
+ })
332
+ const showHeader = createMemo(() => !!(titleValue() || parentID()))
333
+ const projection = createTimelineProjection({
334
+ messages: sessionMessages,
335
+ userMessages: () => props.userMessages,
336
+ sessionMessages: projectedMessages,
337
+ parts: getMsgParts,
338
+ status: sessionStatus,
339
+ showReasoningSummaries: settings.general.showReasoningSummaries,
340
+ inlineComments: settings.general.newLayoutDesigns,
341
+ })
342
+ const activeMessageID = projection.activeMessageID
343
+ const assistantMessagesByParent = projection.assistantMessagesByParent
344
+ const lastAssistantGroupKey = projection.lastAssistantGroupKey
345
+ const messageByID = projection.messageByID
346
+ const messageLastRowIndex = projection.messageLastRowIndex
347
+ const messageRowIndex = projection.messageRowIndex
348
+ const timelineRowByKey = projection.rowByKey
349
+ const timelineRows = projection.rows
350
+
351
+ let prependAnchor: { key: string; offset: number } | undefined
352
+ let prependAnchorFrame: number | undefined
353
+ let prependLoading = false
354
+ const clearPrependAnchor = () => {
355
+ prependLoading = false
356
+ prependAnchor = undefined
357
+ if (prependAnchorFrame === undefined) return
358
+ cancelAnimationFrame(prependAnchorFrame)
359
+ prependAnchorFrame = undefined
360
+ }
361
+ const capturePrependAnchor = () => {
362
+ prependLoading = true
363
+ updatePrependAnchor()
364
+ }
365
+ const updatePrependAnchor = () => {
366
+ const root = listRoot()
367
+ if (!root) return
368
+ const view = root.getBoundingClientRect()
369
+ const anchor = [...root.querySelectorAll<HTMLElement>("[data-timeline-key]")]
370
+ .map((element) => ({ element, rect: element.getBoundingClientRect() }))
371
+ .filter((item) => item.rect.bottom > view.top && item.rect.top < view.bottom)
372
+ .sort((a, b) => a.rect.top - b.rect.top)[0]
373
+ if (!anchor) return
374
+ if (!anchor.element.dataset.timelineKey) return
375
+ prependAnchor = { key: anchor.element.dataset.timelineKey, offset: anchor.rect.top - view.top }
376
+ }
377
+ const restorePrependAnchor = (done: boolean) => {
378
+ if (done) prependLoading = false
379
+ applyPrependAnchor()
380
+ }
381
+ const applyPrependAnchor = () => {
382
+ const root = listRoot()
383
+ if (!root || !prependAnchor) return
384
+ if (prependAnchorFrame !== undefined) cancelAnimationFrame(prependAnchorFrame)
385
+ let frames = 0
386
+ let stable = 0
387
+ const apply = () => {
388
+ prependAnchorFrame = undefined
389
+ const anchor = prependAnchor
390
+ if (!anchor) return
391
+ const element = root.querySelector<HTMLElement>(`[data-timeline-key="${CSS.escape(anchor.key)}"]`)
392
+ const delta = element
393
+ ? element.getBoundingClientRect().top - root.getBoundingClientRect().top - anchor.offset
394
+ : undefined
395
+ if (delta !== undefined && Math.abs(delta) > 0.5) {
396
+ root.scrollTop += delta
397
+ stable = 0
398
+ } else {
399
+ stable += 1
400
+ }
401
+ frames += 1
402
+ if (stable >= 30 || frames >= 180) {
403
+ if (!prependLoading) prependAnchor = undefined
404
+ return
405
+ }
406
+ prependAnchorFrame = requestAnimationFrame(apply)
407
+ }
408
+ prependAnchorFrame = requestAnimationFrame(apply)
409
+ }
410
+
411
+ const [toolOpen, setToolOpen] = createStore<Record<string, boolean | undefined>>(cached?.toolOpen ?? {})
412
+ const [renderOverscan, setRenderOverscan] = createSignal(initialMeasurements?.length || coldBottomMount ? 6 : 20)
413
+ let resizePinnedIndexes: number[] = []
414
+ let resizePinFrame: number | undefined
415
+ let virtualContent: HTMLDivElement | undefined
416
+ const virtualizer = createVirtualizer<HTMLDivElement, HTMLDivElement>({
417
+ get count() {
418
+ return timelineRows().length
419
+ },
420
+ getScrollElement: () => listRoot() ?? null,
421
+ observeElementOffset: observeElementOffsetReconnectAware,
422
+ initialOffset: () => (props.shouldAnchorBottom() ? Number.MAX_SAFE_INTEGER : 0),
423
+ initialMeasurementsCache: initialMeasurements,
424
+ estimateSize: () => timelineFallbackItemSize,
425
+ scrollToFn: (offset, options, instance) => {
426
+ // Expose the computed range before core writes an anchor correction so the browser does not clamp it to the old height.
427
+ if (virtualContent) virtualContent.style.height = `${instance.getTotalSize()}px`
428
+ elementScroll(offset, options, instance)
429
+ },
430
+ get getItemKey() {
431
+ const rows = timelineRows()
432
+ return (index: number) => {
433
+ const row = rows[index]
434
+ // ResizeObserver can report a removed element after its row has left the projection.
435
+ if (!row) return `removed:${index}`
436
+ return TimelineRow.key(row)
437
+ }
438
+ },
439
+ anchorTo: "end",
440
+ followOnAppend: true,
441
+ scrollEndThreshold: 80,
442
+ get scrollMargin() {
443
+ return showHeader() ? 64 : 0
444
+ },
445
+ overscan: 50,
446
+ paddingEnd: 64,
447
+ rangeExtractor: (range) => {
448
+ const id = activeMessageID()
449
+ const active = id ? (messageLastRowIndex().get(id) ?? -1) : -1
450
+ const indexes = defaultRangeExtractor({ ...range, overscan: renderOverscan() })
451
+ return filterVirtualIndexes(
452
+ [...new Set([...resizePinnedIndexes, ...indexes, ...(active < 0 ? [] : [active])])].sort((a, b) => a - b),
453
+ range.count,
454
+ )
455
+ },
456
+ })
457
+ const resizeItem = virtualizer.resizeItem
458
+ let resizeAnchorScheduled = false
459
+ const anchorResizedBottom = () => {
460
+ if (resizeAnchorScheduled || props.hasScrollGesture()) return
461
+ resizeAnchorScheduled = true
462
+ queueMicrotask(() => {
463
+ resizeAnchorScheduled = false
464
+ if (!props.shouldAnchorBottom() || props.hasScrollGesture()) return
465
+ virtualizer.scrollToEnd()
466
+ })
467
+ }
468
+ virtualizer.resizeItem = (index, size) => {
469
+ const item = virtualizer.measurementsCache[index]
470
+ const previous = item ? (virtualizer.itemSizeCache.get(item.key) ?? item.size) : undefined
471
+ const root = listRoot()
472
+ if (root && previous !== undefined && Math.abs(size - previous) > root.clientHeight) {
473
+ const view = root.getBoundingClientRect()
474
+ resizePinnedIndexes = [...root.querySelectorAll<HTMLElement>("[data-index]")]
475
+ .filter((element) => {
476
+ const rect = element.getBoundingClientRect()
477
+ return rect.bottom > view.top && rect.top < view.bottom
478
+ })
479
+ .map((element) => Number(element.dataset.index))
480
+ if (resizePinFrame !== undefined) cancelAnimationFrame(resizePinFrame)
481
+ resizePinFrame = requestAnimationFrame(() => {
482
+ resizePinFrame = requestAnimationFrame(() => {
483
+ resizePinFrame = undefined
484
+ resizePinnedIndexes = []
485
+ })
486
+ })
487
+ }
488
+ resizeItem(index, size)
489
+ if (root && props.shouldAnchorBottom()) anchorResizedBottom()
490
+ }
491
+ virtualizer.shouldAdjustScrollPositionOnItemSizeChange = (item) => {
492
+ if (props.shouldAnchorBottom()) return false
493
+ const first = virtualizer.range?.startIndex
494
+ return first !== undefined && item.index < first
495
+ }
496
+ const virtualItemByKey = createMemo(
497
+ () => new Map(virtualizer.getVirtualItems().map((item) => [item.key, item] as const)),
498
+ )
499
+ const virtualRowKeys = createMemo(() => virtualizer.getVirtualItems().map((item) => item.key as string))
500
+ createEffect(() => {
501
+ props.setRevealMessage?.((id) => {
502
+ const index = messageRowIndex().get(id)
503
+ if (index === undefined) return
504
+ virtualizer.scrollToIndex(index, { align: "center" })
505
+ })
506
+ props.setScrollToEnd?.(() => virtualizer.scrollToEnd())
507
+ props.setHistoryAnchor?.({ capture: capturePrependAnchor, restore: restorePrependAnchor })
508
+ })
509
+
510
+ let overscanFrame: number | undefined
511
+ onMount(() => {
512
+ overscanFrame = requestAnimationFrame(() => {
513
+ if (props.shouldAnchorBottom()) virtualizer.scrollToEnd()
514
+ overscanFrame = requestAnimationFrame(() => {
515
+ overscanFrame = undefined
516
+ if (renderOverscan() < 20) setRenderOverscan(20)
517
+ if (props.shouldAnchorBottom()) virtualizer.scrollToEnd()
518
+ })
519
+ })
520
+ })
521
+
522
+ const maybeAnchorBottom = () => {
523
+ if (timelineRows().length === 0) return
524
+ if (!props.shouldAnchorBottom() || props.hasScrollGesture()) return
525
+ if (resizePinFrame !== undefined) cancelAnimationFrame(resizePinFrame)
526
+ clearPrependAnchor()
527
+ if (prependAnchorFrame !== undefined) cancelAnimationFrame(prependAnchorFrame)
528
+ virtualizer.scrollToEnd()
529
+ }
530
+
531
+ let measuredSessionKey = sessionKey()
532
+ createEffect(() => {
533
+ const key = sessionKey()
534
+ timelineRows().length
535
+ if (measuredSessionKey !== key) {
536
+ measuredSessionKey = key
537
+ virtualizer.measure()
538
+ }
539
+ maybeAnchorBottom()
540
+ })
541
+
542
+ onCleanup(() => {
543
+ clearPrependAnchor()
544
+ timelineCache.delete(ownerSessionKey)
545
+ timelineCache.set(ownerSessionKey, { measurements: virtualizer.takeSnapshot(), toolOpen: { ...toolOpen } })
546
+ while (timelineCache.size > 16) timelineCache.delete(timelineCache.keys().next().value!)
547
+ if (resizePinFrame !== undefined) cancelAnimationFrame(resizePinFrame)
548
+ if (overscanFrame !== undefined) cancelAnimationFrame(overscanFrame)
549
+ props.setRevealMessage?.(() => {})
550
+ props.setScrollToEnd?.(() => {})
551
+ props.setHistoryAnchor?.({ capture: () => {}, restore: () => {} })
552
+ })
553
+
554
+ const [title, setTitle] = createStore({
555
+ draft: "",
556
+ editing: false,
557
+ menuOpen: false,
558
+ pendingRename: false,
559
+ pendingShare: false,
560
+ })
561
+ let titleRef: HTMLInputElement | undefined
562
+
563
+ const [share, setShare] = createStore({
564
+ open: false,
565
+ dismiss: null as "escape" | "outside" | null,
566
+ })
567
+ let more: HTMLButtonElement | undefined
568
+
569
+ const bindListRoot = (root: HTMLDivElement) => {
570
+ if (root === listRoot()) return
571
+ setListRoot(root)
572
+ props.setScrollRef(root)
573
+ }
574
+
575
+ const handleListWheel = (event: WheelEvent & { currentTarget: HTMLDivElement }) => {
576
+ if (!prependLoading) clearPrependAnchor()
577
+ const root = event.currentTarget
578
+ const delta = normalizeWheelDelta({
579
+ deltaY: event.deltaY,
580
+ deltaMode: event.deltaMode,
581
+ rootHeight: root.clientHeight,
582
+ })
583
+ if (!delta) return
584
+ markBoundaryGesture({ root, target: event.target, delta, onMarkScrollGesture: props.onMarkScrollGesture })
585
+ }
586
+
587
+ const handleListTouchStart = (event: TouchEvent) => {
588
+ if (!prependLoading) clearPrependAnchor()
589
+ touchGesture = event.touches[0]?.clientY
590
+ }
591
+
592
+ const handleListTouchMove = (event: TouchEvent & { currentTarget: HTMLDivElement }) => {
593
+ const next = event.touches[0]?.clientY
594
+ const prev = touchGesture
595
+ touchGesture = next
596
+ if (next === undefined || prev === undefined) return
597
+
598
+ const delta = prev - next
599
+ if (!delta) return
600
+
601
+ markBoundaryGesture({
602
+ root: event.currentTarget,
603
+ target: event.target,
604
+ delta,
605
+ onMarkScrollGesture: props.onMarkScrollGesture,
606
+ })
607
+ }
608
+
609
+ const handleListTouchEnd = () => {
610
+ touchGesture = undefined
611
+ }
612
+
613
+ const handleListPointerDown = (event: PointerEvent & { currentTarget: HTMLDivElement }) => {
614
+ if (!prependLoading) clearPrependAnchor()
615
+ props.onMarkScrollGesture(event.target)
616
+ }
617
+
618
+ const handleListPointerMove = (event: PointerEvent) => {
619
+ if (event.buttons !== 1) return
620
+ props.onMarkScrollGesture(event.target)
621
+ }
622
+
623
+ const handleListKeyDown = (event: KeyboardEvent & { currentTarget: HTMLDivElement }) => {
624
+ const key = scrollKey(event)
625
+ if (!key) return
626
+ if (!isScrollKeyTarget(event.target, key)) return
627
+ if (scrollKeyOwner(event.currentTarget, event.target, key) !== event.currentTarget) return
628
+ if (!prependLoading) clearPrependAnchor()
629
+ props.onMarkScrollGesture(event.currentTarget)
630
+ }
631
+
632
+ const handleListScroll = (event: Event & { currentTarget: HTMLDivElement }) => {
633
+ if (prependLoading) updatePrependAnchor()
634
+ props.onScheduleScrollState(event.currentTarget)
635
+ props.onHistoryScroll()
636
+ if (!props.hasScrollGesture()) return
637
+ props.onUserScroll()
638
+ props.onAutoScrollHandleScroll()
639
+ props.onMarkScrollGesture(event.currentTarget)
640
+ }
641
+
642
+ onCleanup(() => {
643
+ props.setScrollRef(undefined)
644
+ })
645
+
646
+ const viewShare = () => {
647
+ const url = shareUrl()
648
+ if (!url) return
649
+ platform.openLink(url)
650
+ }
651
+
652
+ const errorMessage = (err: unknown) => {
653
+ if (err && typeof err === "object" && "data" in err) {
654
+ const data = (err as { data?: { message?: string } }).data
655
+ if (data?.message) return data.message
656
+ }
657
+ if (err instanceof Error) return err.message
658
+ return language.t("common.requestFailed")
659
+ }
660
+
661
+ const shareMutation = useMutation(() => ({
662
+ mutationFn: (id: string) => serverSDK().client.session.share({ sessionID: id }),
663
+ onError: (err) => {
664
+ console.error("Failed to share session", err)
665
+ },
666
+ }))
667
+
668
+ const unshareMutation = useMutation(() => ({
669
+ mutationFn: (id: string) => serverSDK().client.session.unshare({ sessionID: id }),
670
+ onError: (err) => {
671
+ console.error("Failed to unshare session", err)
672
+ },
673
+ }))
674
+
675
+ const titleMutation = useMutation(() => ({
676
+ mutationFn: (input: { id: string; title: string }) =>
677
+ sdk().api.session.rename({ sessionID: input.id, title: input.title }),
678
+ onSuccess: (_, input) => {
679
+ sync().set(
680
+ produce((draft) => {
681
+ const index = draft.session.findIndex((s) => s.id === input.id)
682
+ if (index !== -1) draft.session[index].title = input.title
683
+ }),
684
+ )
685
+ setTitle("editing", false)
686
+ },
687
+ onError: (err) => {
688
+ showToast({
689
+ title: language.t("common.requestFailed"),
690
+ description: errorMessage(err),
691
+ })
692
+ },
693
+ }))
694
+
695
+ const shareSession = () => {
696
+ const id = sessionID()
697
+ if (!id || shareMutation.isPending) return
698
+ if (!shareEnabled()) return
699
+ shareMutation.mutate(id)
700
+ }
701
+
702
+ const unshareSession = () => {
703
+ const id = sessionID()
704
+ if (!id || unshareMutation.isPending) return
705
+ if (!shareEnabled()) return
706
+ unshareMutation.mutate(id)
707
+ }
708
+ const copyShareUrl = () => {
709
+ const url = shareUrl()
710
+ if (!url) return
711
+ void navigator.clipboard
712
+ .writeText(url)
713
+ .then(() =>
714
+ showToast({
715
+ variant: "success",
716
+ icon: "circle-check",
717
+ title: language.t("session.share.copy.copied"),
718
+ description: url,
719
+ }),
720
+ )
721
+ .catch((err: unknown) =>
722
+ showToast({
723
+ title: language.t("common.requestFailed"),
724
+ description: errorMessage(err),
725
+ }),
726
+ )
727
+ }
728
+ const selectShareUrlText: JSX.EventHandler<HTMLDivElement, MouseEvent> = (event) => {
729
+ const selection = window.getSelection()
730
+ if (!selection) return
731
+ const range = document.createRange()
732
+ range.selectNodeContents(event.currentTarget)
733
+ selection.removeAllRanges()
734
+ selection.addRange(range)
735
+ }
736
+
737
+ createEffect(
738
+ on(
739
+ sessionKey,
740
+ () =>
741
+ setTitle({
742
+ draft: "",
743
+ editing: false,
744
+ menuOpen: false,
745
+ pendingRename: false,
746
+ pendingShare: false,
747
+ }),
748
+ { defer: true },
749
+ ),
750
+ )
751
+
752
+ createEffect(
753
+ on(
754
+ () => [parentID(), childTaskDescription()] as const,
755
+ ([id, description]) => {
756
+ if (!id || description) return
757
+ if (sync().data.message[id] !== undefined) return
758
+ void sync().session.sync(id)
759
+ },
760
+ { defer: true },
761
+ ),
762
+ )
763
+
764
+ const openTitleEditor = () => {
765
+ if (!sessionID() || parentID()) return
766
+ setTitle({ editing: true, draft: titleLabel() ?? "" })
767
+ requestAnimationFrame(() => {
768
+ if (!titleRef) return
769
+ titleRef.focus()
770
+ titleRef.select()
771
+ })
772
+ }
773
+
774
+ const closeTitleEditor = () => {
775
+ if (titleMutation.isPending) return
776
+ setTitle("editing", false)
777
+ }
778
+
779
+ const saveTitleEditor = () => {
780
+ const id = sessionID()
781
+ if (!id) return
782
+ if (titleMutation.isPending) return
783
+
784
+ const next = title.draft.trim()
785
+ if (!next || next === (titleLabel() ?? "")) {
786
+ setTitle("editing", false)
787
+ return
788
+ }
789
+
790
+ titleMutation.mutate({ id, title: next })
791
+ }
792
+
793
+ const navigateAfterSessionRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => {
794
+ if (params.id !== sessionID) return
795
+ const href = (id: string) =>
796
+ params.serverKey ? sessionHref(requireServerKey(params.serverKey), id) : legacySessionHref(sdk().directory, id)
797
+ if (parentID) {
798
+ navigate(href(parentID))
799
+ return
800
+ }
801
+ if (nextSessionID) {
802
+ navigate(href(nextSessionID))
803
+ return
804
+ }
805
+ if (params.serverKey) {
806
+ tabs.newDraft({ server: requireServerKey(params.serverKey), directory: sdk().directory })
807
+ return
808
+ }
809
+ navigate(`/${params.dir}/session`)
810
+ }
811
+
812
+ const archiveSession = async (sessionID: string) => {
813
+ const session = sync().session.get(sessionID)
814
+ if (!session) return
815
+
816
+ const sessions = sync().data.session ?? []
817
+ const index = sessions.findIndex((s) => s.id === sessionID)
818
+ const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
819
+
820
+ await sdk()
821
+ .api.session.archive({ sessionID })
822
+ .then(() => {
823
+ sync().set(
824
+ produce((draft) => {
825
+ const index = draft.session.findIndex((s) => s.id === sessionID)
826
+ if (index !== -1) draft.session.splice(index, 1)
827
+ }),
828
+ )
829
+ sync().session.evict(sessionID)
830
+ navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
831
+ notifySessionTabsRemoved({ directory: sdk().directory, sessionIDs: [sessionID] })
832
+ })
833
+ .catch((err) => {
834
+ showToast({
835
+ title: language.t("common.requestFailed"),
836
+ description: errorMessage(err),
837
+ })
838
+ })
839
+ }
840
+
841
+ const deleteSession = async (sessionID: string) => {
842
+ const session = sync().session.get(sessionID)
843
+ if (!session) return false
844
+
845
+ const sessions = (sync().data.session ?? []).filter((s) => !s.parentID && !s.time?.archived)
846
+ const index = sessions.findIndex((s) => s.id === sessionID)
847
+ const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1])
848
+
849
+ const result = await sdk()
850
+ .api.session.remove({ sessionID })
851
+ .then(() => true)
852
+ .catch((err) => {
853
+ showToast({
854
+ title: language.t("session.delete.failed.title"),
855
+ description: errorMessage(err),
856
+ })
857
+ return false
858
+ })
859
+
860
+ if (!result) return false
861
+
862
+ const removed = new Set<string>([sessionID])
863
+ const byParent = new Map<string, string[]>()
864
+ for (const item of sync().data.session) {
865
+ const parentID = item.parentID
866
+ if (!parentID) continue
867
+ const existing = byParent.get(parentID)
868
+ if (existing) {
869
+ existing.push(item.id)
870
+ continue
871
+ }
872
+ byParent.set(parentID, [item.id])
873
+ }
874
+
875
+ const stack = [sessionID]
876
+ while (stack.length) {
877
+ const parentID = stack.pop()
878
+ if (!parentID) continue
879
+
880
+ const children = byParent.get(parentID)
881
+ if (!children) continue
882
+
883
+ for (const child of children) {
884
+ if (removed.has(child)) continue
885
+ removed.add(child)
886
+ stack.push(child)
887
+ }
888
+ }
889
+
890
+ navigateAfterSessionRemoval(sessionID, session.parentID, nextSession?.id)
891
+
892
+ sync().set(
893
+ produce((draft) => {
894
+ draft.session = draft.session.filter((s) => !removed.has(s.id))
895
+ }),
896
+ )
897
+
898
+ for (const id of removed) {
899
+ sync().session.evict(id)
900
+ }
901
+ notifySessionTabsRemoved({ directory: sdk().directory, sessionIDs: [...removed] })
902
+ return true
903
+ }
904
+
905
+ const navigateParent = () => {
906
+ const id = parentID()
907
+ if (!id) return
908
+ navigate(
909
+ params.serverKey ? sessionHref(requireServerKey(params.serverKey), id) : legacySessionHref(sdk().directory, id),
910
+ )
911
+ }
912
+
913
+ function DialogDeleteSession(props: { sessionID: string }) {
914
+ const name = createMemo(
915
+ () => sessionTitle(sync().session.get(props.sessionID)?.title) ?? language.t("command.session.new"),
916
+ )
917
+ const handleDelete = async () => {
918
+ await deleteSession(props.sessionID)
919
+ dialog.close()
920
+ }
921
+
922
+ if (settings.general.newLayoutDesigns())
923
+ return (
924
+ <DialogV2 fit>
925
+ <DialogHeader hideClose>
926
+ <DialogTitleGroup
927
+ title={language.t("session.delete.title")}
928
+ description={language.t("session.delete.confirm", { name: name() })}
929
+ />
930
+ </DialogHeader>
931
+ <DialogFooter>
932
+ <ButtonV2 variant="ghost" onClick={() => dialog.close()}>
933
+ {language.t("common.cancel")}
934
+ </ButtonV2>
935
+ <ButtonV2 variant="danger" onClick={handleDelete}>
936
+ {language.t("session.delete.button")}
937
+ </ButtonV2>
938
+ </DialogFooter>
939
+ </DialogV2>
940
+ )
941
+
942
+ return (
943
+ <Dialog title={language.t("session.delete.title")} fit>
944
+ <div class="flex flex-col gap-4 pl-6 pr-2.5 pb-3">
945
+ <div class="flex flex-col gap-1">
946
+ <span class="text-14-regular text-text-strong">
947
+ {language.t("session.delete.confirm", { name: name() })}
948
+ </span>
949
+ </div>
950
+ <div class="flex justify-end gap-2">
951
+ <Button variant="ghost" size="large" onClick={() => dialog.close()}>
952
+ {language.t("common.cancel")}
953
+ </Button>
954
+ <Button variant="primary" size="large" onClick={handleDelete}>
955
+ {language.t("session.delete.button")}
956
+ </Button>
957
+ </div>
958
+ </div>
959
+ </Dialog>
960
+ )
961
+ }
962
+
963
+ const workingTurn = (userMessageID: string) => sessionStatus().type !== "idle" && activeMessageID() === userMessageID
964
+
965
+ const turnDurationMs = (userMessageID: string) => {
966
+ const message = messageByID().get(userMessageID)
967
+ if (!message || message.role !== "user") return
968
+ const end = (assistantMessagesByParent().get(userMessageID) ?? emptyAssistantMessages).reduce<number | undefined>(
969
+ (max, item) => {
970
+ const completed = item.time.completed
971
+ if (typeof completed !== "number") return max
972
+ if (max === undefined) return completed
973
+ return Math.max(max, completed)
974
+ },
975
+ undefined,
976
+ )
977
+ if (typeof end !== "number") return
978
+ if (end < message.time.created) return
979
+ return end - message.time.created
980
+ }
981
+
982
+ const assistantCopyPartID = (userMessageID: string) => {
983
+ if (workingTurn(userMessageID)) return null
984
+ const messages = assistantMessagesByParent().get(userMessageID) ?? emptyAssistantMessages
985
+
986
+ for (let i = messages.length - 1; i >= 0; i--) {
987
+ const message = messages[i]
988
+ if (!message) continue
989
+
990
+ const parts = getMsgParts(message.id)
991
+ for (let j = parts.length - 1; j >= 0; j--) {
992
+ const part = parts[j]
993
+ if (!part || part.type !== "text" || !part.text?.trim()) continue
994
+ return part.id
995
+ }
996
+ }
997
+ }
998
+
999
+ const renderAssistantPartGroup = (row: Accessor<TimelineRowMap["AssistantPart"]>, onSizeChange?: () => void) => {
1000
+ if (row().group.type === "context") {
1001
+ const parts = createMemo(() => {
1002
+ const group = row().group
1003
+ if (group.type !== "context") return emptyTools
1004
+ return group.refs
1005
+ .map((ref) => getMsgPart(ref.messageID, ref.partID))
1006
+ .filter((part): part is ToolPart => part?.type === "tool")
1007
+ })
1008
+ const contextOpenKey = () => `context:${row().group.key}`
1009
+ const open = createMemo(() => {
1010
+ return toolOpen[contextOpenKey()] === true
1011
+ })
1012
+
1013
+ return (
1014
+ <ContextToolGroup
1015
+ parts={parts()}
1016
+ open={open()}
1017
+ onOpenChange={(value) => setToolOpen(contextOpenKey(), value)}
1018
+ busy={
1019
+ workingTurn(row().userMessageID) && lastAssistantGroupKey().get(row().userMessageID) === row().group.key
1020
+ }
1021
+ onSizeChange={onSizeChange}
1022
+ />
1023
+ )
1024
+ }
1025
+
1026
+ const message = createMemo(() => {
1027
+ const group = row().group
1028
+ if (group.type !== "part") return
1029
+ return messageByID().get(group.ref.messageID)
1030
+ })
1031
+ const part = createMemo(() => {
1032
+ const group = row().group
1033
+ if (group.type !== "part") return
1034
+ return getMsgPart(group.ref.messageID, group.ref.partID)
1035
+ })
1036
+ const defaultOpen = createMemo(() => {
1037
+ const item = part()
1038
+ if (!item) return
1039
+ return partDefaultOpen(item, settings.general.shellToolPartsExpanded(), settings.general.editToolPartsExpanded())
1040
+ })
1041
+
1042
+ return (
1043
+ <Show when={message()}>
1044
+ {(message) => (
1045
+ <Show when={part()}>
1046
+ {(part) => (
1047
+ <MessagePart
1048
+ part={part()}
1049
+ message={message()}
1050
+ showAssistantCopyPartID={assistantCopyPartID(row().userMessageID)}
1051
+ turnDurationMs={turnDurationMs(row().userMessageID)}
1052
+ useV2Actions={settings.general.newLayoutDesigns()}
1053
+ defaultOpen={defaultOpen()}
1054
+ toolOpen={toolOpen[part().id] ?? defaultOpen()}
1055
+ onToolOpenChange={(open) => setToolOpen(part().id, open)}
1056
+ deferToolContent
1057
+ virtualizeDiff={false}
1058
+ onContentRendered={onSizeChange}
1059
+ />
1060
+ )}
1061
+ </Show>
1062
+ )}
1063
+ </Show>
1064
+ )
1065
+ }
1066
+
1067
+ function TimelineRowFrame(input: { row: Accessor<FramedTimelineRow>; children: JSX.Element }) {
1068
+ const anchor = () => {
1069
+ const row = input.row()
1070
+ return row._tag === "CommentStrip" || (row._tag === "UserMessage" && row.anchor)
1071
+ }
1072
+ const previousAssistantPart = () => {
1073
+ const row = input.row()
1074
+ return row._tag === "AssistantPart" && row.previousAssistantPart
1075
+ }
1076
+
1077
+ return (
1078
+ <div
1079
+ id={anchor() ? props.anchor(input.row().userMessageID) : undefined}
1080
+ data-message-id={input.row().userMessageID}
1081
+ data-timeline-row={input.row()._tag}
1082
+ classList={{
1083
+ "min-w-0 w-full max-w-full": true,
1084
+ "md:max-w-200 2xl:max-w-[1000px]": props.centered,
1085
+ "md:mx-auto": props.centered,
1086
+ "pt-3": previousAssistantPart(),
1087
+ }}
1088
+ >
1089
+ <div data-component="session-turn" class="min-w-0 w-full relative" style={{ height: "auto" }}>
1090
+ {input.children}
1091
+ </div>
1092
+ </div>
1093
+ )
1094
+ }
1095
+
1096
+ const renderTimelineRow = (row: Accessor<TimelineRow.TimelineRow>, onSizeChange?: () => void) => {
1097
+ switch (row()._tag) {
1098
+ case "TurnGap":
1099
+ return <div data-timeline-row="TurnGap" aria-hidden="true" class="h-6" />
1100
+ case "CommentStrip": {
1101
+ const commentStripRow = row as Accessor<TimelineRowByTag<"CommentStrip">>
1102
+ const comments = createMemo(() =>
1103
+ getMsgParts(commentStripRow().userMessageID).flatMap((part) => MessageComment.fromPart(part) ?? []),
1104
+ )
1105
+ return (
1106
+ <TimelineRowFrame row={commentStripRow}>
1107
+ <div class="w-full px-4 md:px-5 pb-2">
1108
+ <div class="ml-auto max-w-[82%] overflow-x-auto no-scrollbar">
1109
+ <div class="flex w-max min-w-full justify-end gap-2">
1110
+ <Index each={comments()}>
1111
+ {(comment) => (
1112
+ <div
1113
+ classList={{
1114
+ "shrink-0 max-w-[260px] rounded-[6px] border-border-weak-base bg-background-stronger px-2.5 py-2": true,
1115
+ "border-[0.5px]": settings.general.newLayoutDesigns(),
1116
+ border: !settings.general.newLayoutDesigns(),
1117
+ }}
1118
+ >
1119
+ <div class="flex items-center gap-1.5 min-w-0 text-11-medium text-text-strong">
1120
+ <FileIcon node={{ path: comment().path, type: "file" }} class="size-3.5 shrink-0" />
1121
+ <span class="truncate">{getFilename(comment().path)}</span>
1122
+ <Show when={comment().selection}>
1123
+ {(selection) => (
1124
+ <span class="shrink-0 text-text-weak">
1125
+ {selection().startLine === selection().endLine
1126
+ ? `:${selection().startLine}`
1127
+ : `:${selection().startLine}-${selection().endLine}`}
1128
+ </span>
1129
+ )}
1130
+ </Show>
1131
+ </div>
1132
+ <div class="pt-1 text-12-regular text-text-strong whitespace-pre-wrap break-words">
1133
+ {comment().comment}
1134
+ </div>
1135
+ </div>
1136
+ )}
1137
+ </Index>
1138
+ </div>
1139
+ </div>
1140
+ </div>
1141
+ </TimelineRowFrame>
1142
+ )
1143
+ }
1144
+ case "UserMessage": {
1145
+ const userMessageRow = row as Accessor<TimelineRowByTag<"UserMessage">>
1146
+ const message = createMemo(() => {
1147
+ const m = messageByID().get(userMessageRow().userMessageID)
1148
+ if (m?.role === "user") return m
1149
+ })
1150
+ const messageComments = createMemo(() => {
1151
+ if (!settings.general.newLayoutDesigns()) return []
1152
+ return getMsgParts(userMessageRow().userMessageID).flatMap((part) => MessageComment.fromPart(part) ?? [])
1153
+ })
1154
+ return (
1155
+ <TimelineRowFrame row={userMessageRow}>
1156
+ <Show when={message()}>
1157
+ {(message) => (
1158
+ <div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
1159
+ <div data-slot="session-turn-message-content" aria-live="off">
1160
+ <Message
1161
+ message={message()}
1162
+ parts={getMsgParts(userMessageRow().userMessageID)}
1163
+ actions={props.actions}
1164
+ useV2Actions={settings.general.newLayoutDesigns()}
1165
+ comments={messageComments()}
1166
+ />
1167
+ </div>
1168
+ </div>
1169
+ )}
1170
+ </Show>
1171
+ </TimelineRowFrame>
1172
+ )
1173
+ }
1174
+ case "TurnDivider": {
1175
+ const turnDividerRow = row as Accessor<TimelineRowByTag<"TurnDivider">>
1176
+ return (
1177
+ <TimelineRowFrame row={turnDividerRow}>
1178
+ <div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
1179
+ <div data-slot="session-turn-compaction">
1180
+ <MessageDivider
1181
+ label={language.t(
1182
+ turnDividerRow().label === "compaction" ? "ui.messagePart.compaction" : "ui.message.interrupted",
1183
+ )}
1184
+ />
1185
+ </div>
1186
+ </div>
1187
+ </TimelineRowFrame>
1188
+ )
1189
+ }
1190
+ case "AssistantPart": {
1191
+ const assistantPartRow = row as Accessor<TimelineRowByTag<"AssistantPart">>
1192
+ return (
1193
+ <TimelineRowFrame row={assistantPartRow}>
1194
+ <div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
1195
+ <div
1196
+ data-slot="session-turn-assistant-content"
1197
+ aria-hidden={workingTurn(assistantPartRow().userMessageID)}
1198
+ >
1199
+ {renderAssistantPartGroup(assistantPartRow, onSizeChange)}
1200
+ </div>
1201
+ </div>
1202
+ </TimelineRowFrame>
1203
+ )
1204
+ }
1205
+ case "Thinking": {
1206
+ const thinkingRow = row as Accessor<TimelineRowByTag<"Thinking">>
1207
+ return (
1208
+ <TimelineRowFrame row={thinkingRow}>
1209
+ <div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
1210
+ <TimelineThinkingRow
1211
+ reasoningHeading={thinkingRow().reasoningHeading}
1212
+ showReasoningSummaries={settings.general.showReasoningSummaries()}
1213
+ />
1214
+ </div>
1215
+ </TimelineRowFrame>
1216
+ )
1217
+ }
1218
+ case "Retry": {
1219
+ const retryRow = row as Accessor<TimelineRowByTag<"Retry">>
1220
+ return (
1221
+ <TimelineRowFrame row={retryRow}>
1222
+ <div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
1223
+ <SessionRetry status={sessionStatus()} show={activeMessageID() === retryRow().userMessageID} />
1224
+ </div>
1225
+ </TimelineRowFrame>
1226
+ )
1227
+ }
1228
+ case "DiffSummary": {
1229
+ const diffSummaryRow = row as Accessor<TimelineRowByTag<"DiffSummary">>
1230
+ return (
1231
+ <TimelineRowFrame row={diffSummaryRow}>
1232
+ <div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
1233
+ <TimelineDiffSummaryRow diffs={diffSummaryRow().diffs} />
1234
+ </div>
1235
+ </TimelineRowFrame>
1236
+ )
1237
+ }
1238
+ case "Error": {
1239
+ const errorRow = row as Accessor<TimelineRowByTag<"Error">>
1240
+ return (
1241
+ <TimelineRowFrame row={errorRow}>
1242
+ <div data-slot="session-turn-message-container" class="w-full px-4 md:px-5">
1243
+ <Card variant="error" class="error-card">
1244
+ {errorRow().text}
1245
+ </Card>
1246
+ </div>
1247
+ </TimelineRowFrame>
1248
+ )
1249
+ }
1250
+ }
1251
+ }
1252
+
1253
+ function TimelineRowView(props: { row: TimelineRow.TimelineRow; onSizeChange?: () => void }) {
1254
+ return renderTimelineRow(() => props.row, props.onSizeChange)
1255
+ }
1256
+
1257
+ function VirtualTimelineRow(props: { rowKey: string }) {
1258
+ let element: HTMLDivElement
1259
+ const initialItem = virtualItemByKey().get(props.rowKey)!
1260
+ const initialRow = timelineRowByKey().get(props.rowKey)!
1261
+ const item = createMemo(() => virtualItemByKey().get(props.rowKey) ?? initialItem)
1262
+ const row = createMemo(() => timelineRowByKey().get(props.rowKey) ?? initialRow)
1263
+ const tool = () => {
1264
+ const value = row()
1265
+ if (value._tag !== "AssistantPart" || value.group.type !== "part") return
1266
+ const part = getMsgPart(value.group.ref.messageID, value.group.ref.partID)
1267
+ if (part?.type === "tool") return part
1268
+ }
1269
+ const asyncFile = () => ["edit", "write", "apply_patch"].includes(tool()?.tool ?? "")
1270
+ const [ready, setReady] = createSignal(initialItem.size <= timelineFallbackItemSize || !asyncFile())
1271
+ let contentMeasureFrame: number | undefined
1272
+
1273
+ onMount(() => virtualizer.measureElement(element))
1274
+
1275
+ createEffect(
1276
+ on(
1277
+ () => item().index,
1278
+ () => {
1279
+ virtualizer.measureElement(element)
1280
+ },
1281
+ { defer: true },
1282
+ ),
1283
+ )
1284
+
1285
+ onCleanup(() => {
1286
+ if (contentMeasureFrame !== undefined) cancelAnimationFrame(contentMeasureFrame)
1287
+ })
1288
+
1289
+ return (
1290
+ <div
1291
+ data-timeline-key={props.rowKey}
1292
+ style={{
1293
+ position: "absolute",
1294
+ top: `${item().start - (showHeader() ? 64 : 0)}px`,
1295
+ left: "0",
1296
+ width: "100%",
1297
+ height: `${item().size}px`,
1298
+ overflow: "clip",
1299
+ // Rounded virtual measurements can otherwise clip a framed row's outer paint.
1300
+ "overflow-clip-margin": row()._tag === "TurnGap" ? undefined : "0.5px",
1301
+ }}
1302
+ >
1303
+ <div
1304
+ ref={(value) => {
1305
+ element = value
1306
+ }}
1307
+ data-index={item().index}
1308
+ style={{ "min-height": ready() ? undefined : `${initialItem.size}px` }}
1309
+ >
1310
+ <TimelineRowView
1311
+ row={row()}
1312
+ onSizeChange={() => {
1313
+ setReady(true)
1314
+ if (contentMeasureFrame !== undefined) cancelAnimationFrame(contentMeasureFrame)
1315
+ contentMeasureFrame = scheduleConnectedMeasure(element, virtualizer.measureElement)
1316
+ }}
1317
+ />
1318
+ </div>
1319
+ </div>
1320
+ )
1321
+ }
1322
+
1323
+ return (
1324
+ <div class="relative w-full h-full min-w-0">
1325
+ <div
1326
+ class="absolute left-1/2 -translate-x-1/2 z-[60] pointer-events-none transition-all duration-200 ease-out"
1327
+ classList={{
1328
+ "bottom-8": settings.general.newLayoutDesigns(),
1329
+ "bottom-6": !settings.general.newLayoutDesigns(),
1330
+ "opacity-100 translate-y-0 scale-100": props.scroll.overflow && props.scroll.jump,
1331
+ "opacity-0 translate-y-2 pointer-events-none": !props.scroll.overflow || !props.scroll.jump,
1332
+ "scale-[0.8]": (!props.scroll.overflow || !props.scroll.jump) && settings.general.newLayoutDesigns(),
1333
+ "scale-95": (!props.scroll.overflow || !props.scroll.jump) && !settings.general.newLayoutDesigns(),
1334
+ }}
1335
+ >
1336
+ <Show
1337
+ when={settings.general.newLayoutDesigns()}
1338
+ fallback={
1339
+ <button
1340
+ type="button"
1341
+ aria-label={language.t("session.messages.jumpToLatest")}
1342
+ class="pointer-events-auto flex items-center justify-center w-10 h-8 bg-transparent border-none cursor-pointer p-0 group"
1343
+ onClick={props.onResumeScroll}
1344
+ >
1345
+ <div
1346
+ class="flex items-center justify-center w-8 h-6 rounded-[6px] border border-border-weaker-base bg-[color-mix(in_srgb,var(--surface-raised-stronger-non-alpha)_80%,transparent)] backdrop-blur-[0.75px] transition-colors group-hover:border-[var(--border-weak-base)] group-hover:[--icon-base:var(--icon-hover)]"
1347
+ style={{
1348
+ "box-shadow":
1349
+ "0 51px 60px 0 rgba(0,0,0,0.10), 0 15px 18px 0 rgba(0,0,0,0.12), 0 6.386px 7.513px 0 rgba(0,0,0,0.12), 0 2.31px 2.717px 0 rgba(0,0,0,0.20)",
1350
+ }}
1351
+ >
1352
+ <Icon name="arrow-down-to-line" size="small" />
1353
+ </div>
1354
+ </button>
1355
+ }
1356
+ >
1357
+ <button
1358
+ type="button"
1359
+ aria-label={language.t("session.messages.jumpToLatest")}
1360
+ class="pointer-events-auto flex items-center justify-center w-8 h-7 px-2 py-1.5 rounded-lg border-none cursor-pointer text-v2-text-text-base backdrop-blur-[2px]"
1361
+ style={{
1362
+ background: "color-mix(in srgb, var(--v2-background-bg-base) 92%, transparent)",
1363
+ "box-shadow": "var(--v2-elevation-raised), 0px 2px 8px var(--v2-background-bg-base)",
1364
+ }}
1365
+ onClick={props.onResumeScroll}
1366
+ >
1367
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
1368
+ <path
1369
+ d="M12.3333 8.66665L8 13L3.66667 8.66665M8 12.6667V2.83332"
1370
+ stroke="currentColor"
1371
+ stroke-linecap="square"
1372
+ />
1373
+ </svg>
1374
+ </button>
1375
+ </Show>
1376
+ </div>
1377
+ <ScrollView
1378
+ viewportRef={bindListRoot}
1379
+ onWheel={handleListWheel}
1380
+ onTouchStart={handleListTouchStart}
1381
+ onTouchMove={handleListTouchMove}
1382
+ onTouchEnd={handleListTouchEnd}
1383
+ onTouchCancel={handleListTouchEnd}
1384
+ onPointerDown={handleListPointerDown}
1385
+ onPointerMove={handleListPointerMove}
1386
+ onKeyDown={handleListKeyDown}
1387
+ onScroll={handleListScroll}
1388
+ onClick={props.onAutoScrollInteraction}
1389
+ class="relative min-w-0 w-full h-full"
1390
+ style={{
1391
+ "--sticky-accordion-top": showHeader() ? "48px" : "0px",
1392
+ }}
1393
+ >
1394
+ <Show when={showHeader()}>
1395
+ <div
1396
+ data-session-title
1397
+ classList={{
1398
+ "sticky top-0 z-30": true,
1399
+ "bg-[linear-gradient(to_bottom,var(--v2-background-bg-base)_48px,transparent)]":
1400
+ settings.general.newLayoutDesigns(),
1401
+ "bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]":
1402
+ !settings.general.newLayoutDesigns(),
1403
+ "w-full": true,
1404
+ "pb-4": true,
1405
+ "pr-3": true,
1406
+ "pl-2.5": settings.general.newLayoutDesigns(),
1407
+ "pl-2 md:pl-4": !settings.general.newLayoutDesigns(),
1408
+ "md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered && !settings.general.newLayoutDesigns(),
1409
+ }}
1410
+ >
1411
+ <div class="h-12 w-full flex items-center justify-between gap-2">
1412
+ <div
1413
+ classList={{
1414
+ "flex items-center gap-1 min-w-0 flex-1": true,
1415
+ "pr-3": !settings.general.newLayoutDesigns(),
1416
+ }}
1417
+ >
1418
+ <div class="flex items-center min-w-0 flex-1 w-full">
1419
+ <Show when={parentID()}>
1420
+ <button
1421
+ type="button"
1422
+ data-slot="session-title-parent"
1423
+ class="min-w-0 max-w-[40%] truncate pl-2 text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-faint transition-colors hover:text-v2-text-text-muted"
1424
+ onClick={navigateParent}
1425
+ >
1426
+ {parentTitle()}
1427
+ </button>
1428
+ <span
1429
+ data-slot="session-title-separator"
1430
+ class="-translate-y-[0.5px] pl-2 pr-1 text-[11px] font-medium text-v2-text-text-faint"
1431
+ aria-hidden="true"
1432
+ >
1433
+ /
1434
+ </span>
1435
+ </Show>
1436
+ <Show when={childTitle() || title.editing}>
1437
+ <Show
1438
+ when={title.editing}
1439
+ fallback={
1440
+ <h1
1441
+ data-slot="session-title-child"
1442
+ classList={{
1443
+ "truncate text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-base": true,
1444
+ "w-fit rounded-[6px] px-2 py-1 hover:bg-v2-overlay-simple-overlay-hover":
1445
+ settings.general.newLayoutDesigns(),
1446
+ "grow-1 min-w-0": !settings.general.newLayoutDesigns(),
1447
+ }}
1448
+ onClick={openTitleEditor}
1449
+ >
1450
+ {childTitle()}
1451
+ </h1>
1452
+ }
1453
+ >
1454
+ <InlineInput
1455
+ ref={(el) => {
1456
+ titleRef = el
1457
+ }}
1458
+ data-slot="session-title-child"
1459
+ value={title.draft}
1460
+ disabled={titleMutation.isPending}
1461
+ classList={{
1462
+ "block text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-base": true,
1463
+ "w-full flex-1 grow-1 min-w-0 pl-1 -ml-1 rounded-[6px]": !settings.general.newLayoutDesigns(),
1464
+ "field-sizing-content self-start rounded-[6px] px-2 py-1 ":
1465
+ settings.general.newLayoutDesigns(),
1466
+ }}
1467
+ style={{
1468
+ "--inline-input-shadow": settings.general.newLayoutDesigns()
1469
+ ? "none"
1470
+ : "var(--shadow-xs-border-select)",
1471
+ }}
1472
+ onInput={(event) => setTitle("draft", event.currentTarget.value)}
1473
+ onKeyDown={(event) => {
1474
+ event.stopPropagation()
1475
+ if (event.key === "Enter") {
1476
+ event.preventDefault()
1477
+ void saveTitleEditor()
1478
+ return
1479
+ }
1480
+ if (event.key === "Escape") {
1481
+ event.preventDefault()
1482
+ closeTitleEditor()
1483
+ }
1484
+ }}
1485
+ onBlur={closeTitleEditor}
1486
+ />
1487
+ </Show>
1488
+ </Show>
1489
+ </div>
1490
+ </div>
1491
+ <Show when={sessionID()} keyed>
1492
+ {(id) => (
1493
+ <div
1494
+ classList={{
1495
+ "shrink-0 flex items-center": true,
1496
+ "gap-2": settings.general.newLayoutDesigns(),
1497
+ "gap-3": !settings.general.newLayoutDesigns(),
1498
+ }}
1499
+ >
1500
+ <SessionContextUsage
1501
+ placement="bottom"
1502
+ buttonAppearance={settings.general.newLayoutDesigns() ? "v2" : "default"}
1503
+ />
1504
+ <Show when={!parentID()}>
1505
+ <Show
1506
+ when={settings.general.newLayoutDesigns()}
1507
+ fallback={
1508
+ <DropdownMenu
1509
+ gutter={4}
1510
+ placement="bottom-end"
1511
+ open={title.menuOpen}
1512
+ onOpenChange={(open) => {
1513
+ setTitle("menuOpen", open)
1514
+ if (open) return
1515
+ }}
1516
+ >
1517
+ <DropdownMenu.Trigger
1518
+ as={IconButton}
1519
+ icon="dot-grid"
1520
+ variant="ghost"
1521
+ class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
1522
+ classList={{
1523
+ "bg-surface-base-active": share.open || title.pendingShare,
1524
+ }}
1525
+ aria-label={language.t("common.moreOptions")}
1526
+ aria-expanded={title.menuOpen || share.open || title.pendingShare}
1527
+ ref={(el: HTMLButtonElement) => {
1528
+ more = el
1529
+ }}
1530
+ />
1531
+ <DropdownMenu.Portal>
1532
+ <DropdownMenu.Content
1533
+ style={{ "min-width": "104px" }}
1534
+ onCloseAutoFocus={(event) => {
1535
+ if (title.pendingRename) {
1536
+ event.preventDefault()
1537
+ setTitle("pendingRename", false)
1538
+ openTitleEditor()
1539
+ return
1540
+ }
1541
+ if (title.pendingShare) {
1542
+ event.preventDefault()
1543
+ requestAnimationFrame(() => {
1544
+ setShare({ open: true, dismiss: null })
1545
+ setTitle("pendingShare", false)
1546
+ })
1547
+ }
1548
+ }}
1549
+ >
1550
+ <DropdownMenu.Item
1551
+ onSelect={() => {
1552
+ setTitle("pendingRename", true)
1553
+ setTitle("menuOpen", false)
1554
+ }}
1555
+ >
1556
+ <DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel>
1557
+ </DropdownMenu.Item>
1558
+ <Show when={shareEnabled()}>
1559
+ <DropdownMenu.Item
1560
+ onSelect={() => {
1561
+ setTitle({ pendingShare: true, menuOpen: false })
1562
+ }}
1563
+ >
1564
+ <DropdownMenu.ItemLabel>
1565
+ {language.t("session.share.action.share")}
1566
+ </DropdownMenu.ItemLabel>
1567
+ </DropdownMenu.Item>
1568
+ </Show>
1569
+ <DropdownMenu.Item onSelect={() => void archiveSession(id)}>
1570
+ <DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel>
1571
+ </DropdownMenu.Item>
1572
+ <DropdownMenu.Separator />
1573
+ <DropdownMenu.Item
1574
+ onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id} />)}
1575
+ >
1576
+ <DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel>
1577
+ </DropdownMenu.Item>
1578
+ </DropdownMenu.Content>
1579
+ </DropdownMenu.Portal>
1580
+ </DropdownMenu>
1581
+ }
1582
+ >
1583
+ <MenuV2
1584
+ gutter={6}
1585
+ placement="bottom-end"
1586
+ open={title.menuOpen}
1587
+ onOpenChange={(open) => {
1588
+ setTitle("menuOpen", open)
1589
+ if (open) return
1590
+ }}
1591
+ >
1592
+ <MenuV2.Trigger
1593
+ as={IconButtonV2}
1594
+ icon={<IconV2 name="outline-dots" />}
1595
+ variant="ghost-muted"
1596
+ size="large"
1597
+ state={share.open || title.pendingShare ? "pressed" : undefined}
1598
+ aria-label={language.t("common.moreOptions")}
1599
+ aria-expanded={title.menuOpen || share.open || title.pendingShare}
1600
+ ref={(el: HTMLButtonElement) => {
1601
+ more = el
1602
+ }}
1603
+ />
1604
+ <MenuV2.Portal>
1605
+ <MenuV2.Content
1606
+ style={{ width: "120px", "min-width": "120px" }}
1607
+ onCloseAutoFocus={(event) => {
1608
+ if (title.pendingRename) {
1609
+ event.preventDefault()
1610
+ setTitle("pendingRename", false)
1611
+ openTitleEditor()
1612
+ return
1613
+ }
1614
+ if (title.pendingShare) {
1615
+ event.preventDefault()
1616
+ requestAnimationFrame(() => {
1617
+ setShare({ open: true, dismiss: null })
1618
+ setTitle("pendingShare", false)
1619
+ })
1620
+ }
1621
+ }}
1622
+ >
1623
+ <MenuV2.Item
1624
+ onSelect={() => {
1625
+ setTitle("pendingRename", true)
1626
+ setTitle("menuOpen", false)
1627
+ }}
1628
+ >
1629
+ {language.t("common.rename")}
1630
+ </MenuV2.Item>
1631
+ <Show when={shareEnabled()}>
1632
+ <MenuV2.Item
1633
+ onSelect={() => {
1634
+ setTitle({ pendingShare: true, menuOpen: false })
1635
+ }}
1636
+ >
1637
+ {language.t("session.share.action.share")}...
1638
+ </MenuV2.Item>
1639
+ </Show>
1640
+ <MenuV2.Item onSelect={() => void archiveSession(id)}>
1641
+ {language.t("common.archive")}
1642
+ </MenuV2.Item>
1643
+ <MenuV2.Separator />
1644
+ <MenuV2.Item onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id} />)}>
1645
+ {language.t("common.delete")}...
1646
+ </MenuV2.Item>
1647
+ </MenuV2.Content>
1648
+ </MenuV2.Portal>
1649
+ </MenuV2>
1650
+ </Show>
1651
+
1652
+ <KobaltePopover
1653
+ open={share.open}
1654
+ anchorRef={() => more}
1655
+ placement="bottom-end"
1656
+ gutter={settings.general.newLayoutDesigns() ? 6 : 4}
1657
+ modal={false}
1658
+ onOpenChange={(open) => {
1659
+ if (open) setShare("dismiss", null)
1660
+ setShare("open", open)
1661
+ }}
1662
+ >
1663
+ <KobaltePopover.Portal>
1664
+ <KobaltePopover.Content
1665
+ data-component="popover-content"
1666
+ classList={{
1667
+ "flex w-80 max-w-none flex-col items-start gap-3 rounded-[10px] border-0 bg-v2-background-bg-layer-01 p-3 shadow-[var(--v2-elevation-floating)]":
1668
+ settings.general.newLayoutDesigns(),
1669
+ }}
1670
+ style={{ "min-width": "320px" }}
1671
+ onEscapeKeyDown={(event) => {
1672
+ setShare({ dismiss: "escape", open: false })
1673
+ event.preventDefault()
1674
+ event.stopPropagation()
1675
+ }}
1676
+ onPointerDownOutside={() => {
1677
+ setShare({ dismiss: "outside", open: false })
1678
+ }}
1679
+ onFocusOutside={() => {
1680
+ setShare({ dismiss: "outside", open: false })
1681
+ }}
1682
+ onCloseAutoFocus={(event) => {
1683
+ if (share.dismiss === "outside") event.preventDefault()
1684
+ setShare("dismiss", null)
1685
+ }}
1686
+ >
1687
+ <Show
1688
+ when={settings.general.newLayoutDesigns()}
1689
+ fallback={
1690
+ <div class="flex flex-col p-3">
1691
+ <div class="flex flex-col gap-1">
1692
+ <div class="text-13-medium text-text-strong">
1693
+ {language.t("session.share.popover.title")}
1694
+ </div>
1695
+ <div class="text-12-regular text-text-weak">
1696
+ {shareUrl()
1697
+ ? language.t("session.share.popover.description.shared")
1698
+ : language.t("session.share.popover.description.unshared")}
1699
+ </div>
1700
+ </div>
1701
+ <div class="mt-3 flex flex-col gap-2">
1702
+ <Show
1703
+ when={shareUrl()}
1704
+ fallback={
1705
+ <Button
1706
+ size="large"
1707
+ variant="primary"
1708
+ class="w-full"
1709
+ onClick={shareSession}
1710
+ disabled={shareMutation.isPending}
1711
+ >
1712
+ {shareMutation.isPending
1713
+ ? language.t("session.share.action.publishing")
1714
+ : language.t("session.share.action.publish")}
1715
+ </Button>
1716
+ }
1717
+ >
1718
+ <div class="flex flex-col gap-2">
1719
+ <TextField
1720
+ value={shareUrl() ?? ""}
1721
+ readOnly
1722
+ copyable
1723
+ copyKind="link"
1724
+ tabIndex={-1}
1725
+ class="w-full"
1726
+ />
1727
+ <div class="grid grid-cols-2 gap-2">
1728
+ <Button
1729
+ size="large"
1730
+ variant="secondary"
1731
+ class="w-full shadow-none border border-border-weak-base"
1732
+ onClick={unshareSession}
1733
+ disabled={unshareMutation.isPending}
1734
+ >
1735
+ {unshareMutation.isPending
1736
+ ? language.t("session.share.action.unpublishing")
1737
+ : language.t("session.share.action.unpublish")}
1738
+ </Button>
1739
+ <Button
1740
+ size="large"
1741
+ variant="primary"
1742
+ class="w-full"
1743
+ onClick={viewShare}
1744
+ disabled={unshareMutation.isPending}
1745
+ >
1746
+ {language.t("session.share.action.view")}
1747
+ </Button>
1748
+ </div>
1749
+ </div>
1750
+ </Show>
1751
+ </div>
1752
+ </div>
1753
+ }
1754
+ >
1755
+ <div class="flex w-full flex-col gap-1.5 px-0.5 pt-0.5">
1756
+ <div class="select-none text-[13px] font-[530] leading-none tracking-[-0.04px] text-v2-text-text-base [font-variation-settings:'slnt'_0]">
1757
+ {language.t("session.share.popover.title")}
1758
+ </div>
1759
+ <div class="select-none text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-muted [font-variation-settings:'slnt'_0]">
1760
+ {shareUrl()
1761
+ ? language.t("session.share.popover.description.shared")
1762
+ : language.t("session.share.popover.description.unshared")}
1763
+ </div>
1764
+ </div>
1765
+ <div class="flex w-full flex-col gap-2">
1766
+ <Show
1767
+ when={shareUrl()}
1768
+ fallback={
1769
+ <ButtonV2
1770
+ variant="contrast"
1771
+ class="w-full"
1772
+ onClick={shareSession}
1773
+ disabled={shareMutation.isPending}
1774
+ >
1775
+ {shareMutation.isPending
1776
+ ? language.t("session.share.action.publishing")
1777
+ : language.t("session.share.action.publish")}
1778
+ </ButtonV2>
1779
+ }
1780
+ >
1781
+ <div class="flex flex-col gap-2">
1782
+ <div
1783
+ class="flex h-8 w-full items-center gap-1.5 rounded-[6px] py-1 pl-2.5 pr-1.5 shadow-[var(--v2-elevation-button-neutral)]"
1784
+ style={{
1785
+ background:
1786
+ "linear-gradient(180deg, var(--v2-alpha-light-2) 0%, var(--v2-alpha-light-0) 100%), var(--v2-background-bg-button-neutral)",
1787
+ }}
1788
+ >
1789
+ <div
1790
+ class="min-w-0 flex-1 truncate select-text cursor-text text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base [font-variation-settings:'slnt'_0]"
1791
+ onClick={selectShareUrlText}
1792
+ >
1793
+ {shareUrl()}
1794
+ </div>
1795
+ <IconButtonV2
1796
+ type="button"
1797
+ size="small"
1798
+ variant="ghost-muted"
1799
+ icon={<IconV2 name="outline-copy" />}
1800
+ aria-label={language.t("session.share.copy.copyLink")}
1801
+ onClick={copyShareUrl}
1802
+ />
1803
+ <IconButtonV2
1804
+ type="button"
1805
+ size="small"
1806
+ variant="ghost-muted"
1807
+ icon={<IconV2 name="outline-square-arrow" />}
1808
+ aria-label={language.t("session.share.action.view")}
1809
+ onClick={viewShare}
1810
+ disabled={unshareMutation.isPending}
1811
+ />
1812
+ </div>
1813
+ <div class="flex w-full">
1814
+ <ButtonV2
1815
+ variant="outline"
1816
+ class="w-full"
1817
+ onClick={unshareSession}
1818
+ disabled={unshareMutation.isPending}
1819
+ >
1820
+ {unshareMutation.isPending
1821
+ ? language.t("session.share.action.unpublishing")
1822
+ : language.t("session.share.action.unpublish")}
1823
+ </ButtonV2>
1824
+ </div>
1825
+ </div>
1826
+ </Show>
1827
+ </div>
1828
+ </Show>
1829
+ </KobaltePopover.Content>
1830
+ </KobaltePopover.Portal>
1831
+ </KobaltePopover>
1832
+ </Show>
1833
+ </div>
1834
+ )}
1835
+ </Show>
1836
+ </div>
1837
+ </div>
1838
+ </Show>
1839
+ <div
1840
+ data-timeline-virtual-content
1841
+ ref={(element) => {
1842
+ virtualContent = element
1843
+ props.setContentRef(element)
1844
+ }}
1845
+ style={{
1846
+ height: `${virtualizer.getTotalSize()}px`,
1847
+ position: "relative",
1848
+ width: "100%",
1849
+ }}
1850
+ >
1851
+ <For each={virtualRowKeys()}>{(rowKey) => <VirtualTimelineRow rowKey={rowKey} />}</For>
1852
+ <Show when={timelineRows().length > 0}>
1853
+ <div
1854
+ data-timeline-row="bottom-spacer"
1855
+ aria-hidden="true"
1856
+ class="h-16 absolute top-0 left-0 w-full"
1857
+ style={{ transform: `translateY(${virtualizer.getTotalSize() - 64}px)` }}
1858
+ />
1859
+ </Show>
1860
+ </div>
1861
+ </ScrollView>
1862
+ </div>
1863
+ )
1864
+ }