@opengeni/react 3.3.2-canary.1 → 3.4.0

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 (855) hide show
  1. package/README.md +769 -22
  2. package/dist/accounts.js +2 -0
  3. package/dist/accounts.js.map +1 -1
  4. package/dist/approvals.d.ts +2 -1
  5. package/dist/artifacts-document.js +5 -3
  6. package/dist/artifacts-presentation.js +5 -3
  7. package/dist/artifacts-spreadsheet.d.ts +1 -1
  8. package/dist/artifacts-spreadsheet.js +5 -3
  9. package/dist/artifacts.d.ts +5 -1
  10. package/dist/artifacts.js +536 -42
  11. package/dist/artifacts.js.map +1 -1
  12. package/dist/browser-viewer-ZWR6MWJ7.js +19 -0
  13. package/dist/capability-catalog-row.d.ts +20 -0
  14. package/dist/capability-logo-fallback.d.ts +3 -0
  15. package/dist/chat-interactive-block-4EUKJXUO.js +14 -0
  16. package/dist/chunk-2DDJ6YH6.js +41 -0
  17. package/dist/chunk-2DDJ6YH6.js.map +1 -0
  18. package/dist/chunk-2HVLQ26H.js +183 -0
  19. package/dist/chunk-2HVLQ26H.js.map +1 -0
  20. package/dist/{chunk-ZKNTK7IA.js → chunk-2I6R3GCZ.js} +4284 -3983
  21. package/dist/chunk-2I6R3GCZ.js.map +1 -0
  22. package/dist/{chunk-JAQ5K7W6.js → chunk-2SFFYIH3.js} +1178 -166
  23. package/dist/chunk-2SFFYIH3.js.map +1 -0
  24. package/dist/chunk-3PVDYTTB.js +24 -0
  25. package/dist/chunk-3PVDYTTB.js.map +1 -0
  26. package/dist/chunk-3RJKUVCA.js +14068 -0
  27. package/dist/chunk-3RJKUVCA.js.map +1 -0
  28. package/dist/chunk-3UZCWK46.js +298 -0
  29. package/dist/chunk-3UZCWK46.js.map +1 -0
  30. package/dist/chunk-47CKUXTW.js +293 -0
  31. package/dist/chunk-47CKUXTW.js.map +1 -0
  32. package/dist/chunk-4QYP6QUI.js +109 -0
  33. package/dist/chunk-4QYP6QUI.js.map +1 -0
  34. package/dist/chunk-5EY26VZ7.js +1053 -0
  35. package/dist/chunk-5EY26VZ7.js.map +1 -0
  36. package/dist/chunk-5I4VRWBB.js +56 -0
  37. package/dist/chunk-5I4VRWBB.js.map +1 -0
  38. package/dist/chunk-5WOR7UCL.js +191 -0
  39. package/dist/chunk-5WOR7UCL.js.map +1 -0
  40. package/dist/chunk-5YJLGOM5.js +51 -0
  41. package/dist/chunk-5YJLGOM5.js.map +1 -0
  42. package/dist/chunk-6JFN3OWG.js +386 -0
  43. package/dist/chunk-6JFN3OWG.js.map +1 -0
  44. package/dist/chunk-7QJT5O4S.js +35 -0
  45. package/dist/chunk-7QJT5O4S.js.map +1 -0
  46. package/dist/chunk-A55KV72M.js +402 -0
  47. package/dist/chunk-A55KV72M.js.map +1 -0
  48. package/dist/chunk-ADWCLI4G.js +2028 -0
  49. package/dist/chunk-ADWCLI4G.js.map +1 -0
  50. package/dist/chunk-B27E3V37.js +29 -0
  51. package/dist/chunk-B27E3V37.js.map +1 -0
  52. package/dist/chunk-B7TC2A33.js +83 -0
  53. package/dist/chunk-B7TC2A33.js.map +1 -0
  54. package/dist/chunk-BLL56F7R.js +385 -0
  55. package/dist/chunk-BLL56F7R.js.map +1 -0
  56. package/dist/chunk-BOFEQC6K.js +162 -0
  57. package/dist/chunk-BOFEQC6K.js.map +1 -0
  58. package/dist/chunk-CM7LVUKC.js +35 -0
  59. package/dist/chunk-CM7LVUKC.js.map +1 -0
  60. package/dist/chunk-CORUP6JB.js +122 -0
  61. package/dist/chunk-CORUP6JB.js.map +1 -0
  62. package/dist/chunk-CSX5FFKZ.js +35 -0
  63. package/dist/chunk-CSX5FFKZ.js.map +1 -0
  64. package/dist/chunk-EGVHJKGJ.js +3125 -0
  65. package/dist/chunk-EGVHJKGJ.js.map +1 -0
  66. package/dist/{chunk-NEXLLTP4.js → chunk-G76LSA76.js} +6 -5
  67. package/dist/chunk-G76LSA76.js.map +1 -0
  68. package/dist/{chunk-PEI6RANI.js → chunk-GQMRL52F.js} +311 -75
  69. package/dist/chunk-GQMRL52F.js.map +1 -0
  70. package/dist/{chunk-ELIYWBZR.js → chunk-GUUXOISA.js} +1113 -457
  71. package/dist/chunk-GUUXOISA.js.map +1 -0
  72. package/dist/{chunk-HUIKIDM5.js → chunk-I7X2P4FU.js} +225 -116
  73. package/dist/chunk-I7X2P4FU.js.map +1 -0
  74. package/dist/chunk-IGFZFLI2.js +1390 -0
  75. package/dist/chunk-IGFZFLI2.js.map +1 -0
  76. package/dist/{chunk-4MDLEHXL.js → chunk-IMSUVYQB.js} +26 -13
  77. package/dist/chunk-IMSUVYQB.js.map +1 -0
  78. package/dist/chunk-IOYHDEP3.js +38 -0
  79. package/dist/chunk-IOYHDEP3.js.map +1 -0
  80. package/dist/chunk-IZV5FTJC.js +55 -0
  81. package/dist/chunk-IZV5FTJC.js.map +1 -0
  82. package/dist/chunk-KF33GTWN.js +343 -0
  83. package/dist/chunk-KF33GTWN.js.map +1 -0
  84. package/dist/chunk-KSGQ5ODA.js +457 -0
  85. package/dist/chunk-KSGQ5ODA.js.map +1 -0
  86. package/dist/{chunk-ZPBTI3TE.js → chunk-KZC6QRNR.js} +1513 -697
  87. package/dist/chunk-KZC6QRNR.js.map +1 -0
  88. package/dist/chunk-LJUMYUBO.js +218 -0
  89. package/dist/chunk-LJUMYUBO.js.map +1 -0
  90. package/dist/chunk-LSW2OJVV.js +380 -0
  91. package/dist/chunk-LSW2OJVV.js.map +1 -0
  92. package/dist/chunk-M357MHH3.js +2 -0
  93. package/dist/chunk-M357MHH3.js.map +1 -0
  94. package/dist/chunk-MK2TQO4Y.js +24 -0
  95. package/dist/chunk-MK2TQO4Y.js.map +1 -0
  96. package/dist/chunk-N7S72EXK.js +25 -0
  97. package/dist/chunk-N7S72EXK.js.map +1 -0
  98. package/dist/{chunk-YVCHAQZY.js → chunk-NKMYZFHA.js} +616 -621
  99. package/dist/chunk-NKMYZFHA.js.map +1 -0
  100. package/dist/chunk-NX4ADH73.js +14 -0
  101. package/dist/chunk-NX4ADH73.js.map +1 -0
  102. package/dist/chunk-O7F6UJO4.js +71 -0
  103. package/dist/chunk-O7F6UJO4.js.map +1 -0
  104. package/dist/chunk-OYQQJL3J.js +16 -0
  105. package/dist/chunk-OYQQJL3J.js.map +1 -0
  106. package/dist/{chunk-H5YGYNXS.js → chunk-P4BNUO57.js} +60 -17
  107. package/dist/chunk-P4BNUO57.js.map +1 -0
  108. package/dist/{chunk-JVYQRRC4.js → chunk-PEHTX3V7.js} +21 -1
  109. package/dist/chunk-PEHTX3V7.js.map +1 -0
  110. package/dist/chunk-PMV53IMM.js +26 -0
  111. package/dist/chunk-PMV53IMM.js.map +1 -0
  112. package/dist/{chunk-KCGXPP5V.js → chunk-PYEOPA4M.js} +13 -7
  113. package/dist/chunk-PYEOPA4M.js.map +1 -0
  114. package/dist/{chunk-KZ73RL76.js → chunk-QHUE7E4N.js} +8 -3
  115. package/dist/chunk-QHUE7E4N.js.map +1 -0
  116. package/dist/chunk-QV3DJVRZ.js +11 -0
  117. package/dist/chunk-QV3DJVRZ.js.map +1 -0
  118. package/dist/chunk-R5SBNG7S.js +810 -0
  119. package/dist/chunk-R5SBNG7S.js.map +1 -0
  120. package/dist/chunk-RI42H5IU.js +326 -0
  121. package/dist/chunk-RI42H5IU.js.map +1 -0
  122. package/dist/chunk-S2QH6EZ5.js +211 -0
  123. package/dist/chunk-S2QH6EZ5.js.map +1 -0
  124. package/dist/chunk-SHLZMAJN.js +304 -0
  125. package/dist/chunk-SHLZMAJN.js.map +1 -0
  126. package/dist/chunk-TGXK4BAZ.js +293 -0
  127. package/dist/chunk-TGXK4BAZ.js.map +1 -0
  128. package/dist/{chunk-5YK2ZDIG.js → chunk-TNEZNMSN.js} +4 -10
  129. package/dist/chunk-TNEZNMSN.js.map +1 -0
  130. package/dist/{chunk-22KP6LR5.js → chunk-TXS65BB2.js} +3 -1
  131. package/dist/{chunk-22KP6LR5.js.map → chunk-TXS65BB2.js.map} +1 -1
  132. package/dist/chunk-UNJPZGNK.js +63 -0
  133. package/dist/chunk-UNJPZGNK.js.map +1 -0
  134. package/dist/{chunk-IOLVN74D.js → chunk-V5ZUJT2T.js} +175 -42
  135. package/dist/chunk-V5ZUJT2T.js.map +1 -0
  136. package/dist/chunk-WBJY57A3.js +402 -0
  137. package/dist/chunk-WBJY57A3.js.map +1 -0
  138. package/dist/chunk-WIUKB3LD.js +1911 -0
  139. package/dist/chunk-WIUKB3LD.js.map +1 -0
  140. package/dist/{chunk-ICA6R6TW.js → chunk-WNN7NO2N.js} +1451 -355
  141. package/dist/chunk-WNN7NO2N.js.map +1 -0
  142. package/dist/{chunk-E5KISKFG.js → chunk-X5UVUIMF.js} +1709 -1184
  143. package/dist/chunk-X5UVUIMF.js.map +1 -0
  144. package/dist/chunk-XDA5QJE7.js +402 -0
  145. package/dist/chunk-XDA5QJE7.js.map +1 -0
  146. package/dist/{chunk-VZTQ73XT.js → chunk-XEA5BCMA.js} +3 -1
  147. package/dist/{chunk-VZTQ73XT.js.map → chunk-XEA5BCMA.js.map} +1 -1
  148. package/dist/chunk-XHF7OYAN.js +1150 -0
  149. package/dist/chunk-XHF7OYAN.js.map +1 -0
  150. package/dist/chunk-YSRBFJMQ.js +43 -0
  151. package/dist/chunk-YSRBFJMQ.js.map +1 -0
  152. package/dist/chunk-ZOZAPAUO.js +174 -0
  153. package/dist/chunk-ZOZAPAUO.js.map +1 -0
  154. package/dist/chunk-ZQV5W7YC.js +809 -0
  155. package/dist/chunk-ZQV5W7YC.js.map +1 -0
  156. package/dist/chunk-ZUWF4NF7.js +13 -0
  157. package/dist/chunk-ZUWF4NF7.js.map +1 -0
  158. package/dist/client.d.ts +16 -7
  159. package/dist/clipboard.js +3 -1
  160. package/dist/commands/types.d.ts +2 -0
  161. package/dist/components/anthropic-mark.d.ts +3 -0
  162. package/dist/components/approval-surface.d.ts +19 -6
  163. package/dist/components/artifacts/artifact-chrome.d.ts +120 -0
  164. package/dist/components/artifacts/artifact-sandbox.d.ts +30 -0
  165. package/dist/components/artifacts/artifact-surface.d.ts +4 -2
  166. package/dist/components/artifacts/chat-interactive-block.d.ts +29 -0
  167. package/dist/components/artifacts/deferred-chat-media.d.ts +15 -0
  168. package/dist/components/artifacts/document-editor.d.ts +4 -2
  169. package/dist/components/artifacts/editable-artifact-ui.d.ts +2 -1
  170. package/dist/components/artifacts/editable-artifact-view.d.ts +48 -0
  171. package/dist/components/artifacts/editable-document.d.ts +1 -1
  172. package/dist/components/artifacts/editable-presentation.d.ts +2 -2
  173. package/dist/components/artifacts/editable-spreadsheet.d.ts +8 -3
  174. package/dist/components/artifacts/index.d.ts +9 -2
  175. package/dist/components/artifacts/inline-html-document.d.ts +2 -0
  176. package/dist/components/artifacts/latest-task-runner.d.ts +15 -0
  177. package/dist/components/artifacts/presentation-editor.d.ts +6 -2
  178. package/dist/components/artifacts/published-html-artifact-frame.d.ts +23 -3
  179. package/dist/components/artifacts/session-artifact-viewer.d.ts +39 -0
  180. package/dist/components/artifacts/site-view.d.ts +26 -0
  181. package/dist/components/artifacts/spreadsheet-command-queue.d.ts +20 -0
  182. package/dist/components/artifacts/spreadsheet-download.d.ts +7 -0
  183. package/dist/components/artifacts/spreadsheet-general-display.d.ts +24 -0
  184. package/dist/components/artifacts/spreadsheet-grid.d.ts +10 -3
  185. package/dist/components/artifacts/spreadsheet-resize.d.ts +50 -0
  186. package/dist/components/artifacts/spreadsheet-submissions.d.ts +9 -0
  187. package/dist/components/artifacts/visualization-assets.gen.d.ts +2 -0
  188. package/dist/components/browser-select-control.d.ts +16 -0
  189. package/dist/components/browser-viewer.d.ts +11 -2
  190. package/dist/components/chat-composer.d.ts +21 -3
  191. package/dist/components/child-session-link.d.ts +9 -0
  192. package/dist/components/claude-mark.d.ts +3 -0
  193. package/dist/components/code-editor.d.ts +1 -4
  194. package/dist/components/composer-transcription-control.d.ts +7 -0
  195. package/dist/components/composer.d.ts +34 -67
  196. package/dist/components/computer-viewer.d.ts +1 -1
  197. package/dist/components/copy-button.d.ts +3 -1
  198. package/dist/components/desktop-viewer.d.ts +1 -1
  199. package/dist/components/grok-mark.d.ts +26 -0
  200. package/dist/components/human-input-form.d.ts +28 -32
  201. package/dist/components/human-input-surface.d.ts +7 -2
  202. package/dist/components/linkified-text.d.ts +18 -0
  203. package/dist/components/machine-input-display.d.ts +22 -1
  204. package/dist/components/machines/agent-status-icon.d.ts +6 -0
  205. package/dist/components/machines/machine-detail.d.ts +7 -1
  206. package/dist/components/machines/machine-status.d.ts +48 -0
  207. package/dist/components/markdown-table-layout.d.ts +18 -0
  208. package/dist/components/markdown.d.ts +37 -4
  209. package/dist/components/message-timeline.d.ts +86 -7
  210. package/dist/components/model-mark.d.ts +29 -0
  211. package/dist/components/model-policy-picker-menu.d.ts +9 -0
  212. package/dist/components/model-policy-picker.d.ts +35 -28
  213. package/dist/components/new-conversation.d.ts +32 -0
  214. package/dist/components/open-geni-chat.d.ts +73 -0
  215. package/dist/components/open-geni-links.d.ts +68 -0
  216. package/dist/components/openrouter-mark.d.ts +6 -0
  217. package/dist/components/opper-mark.d.ts +7 -0
  218. package/dist/components/preview-loading.d.ts +2 -0
  219. package/dist/components/provider-recovery-notice.d.ts +16 -0
  220. package/dist/components/queue-item-content.d.ts +9 -0
  221. package/dist/components/rehype-secret-refs.d.ts +23 -0
  222. package/dist/components/remark-soft-line-breaks.d.ts +38 -0
  223. package/dist/components/sandbox-files.d.ts +3 -1
  224. package/dist/components/sandbox-workspace.d.ts +18 -0
  225. package/dist/components/secret-question-field.d.ts +31 -0
  226. package/dist/components/secret-ref-chip.d.ts +35 -0
  227. package/dist/components/session-capability-frame.d.ts +37 -0
  228. package/dist/components/session-chrome.d.ts +57 -8
  229. package/dist/components/session-commands-panel.d.ts +6 -0
  230. package/dist/components/session-commands.d.ts +6 -0
  231. package/dist/components/session-connection-request.d.ts +8 -0
  232. package/dist/components/session-conversation.d.ts +157 -0
  233. package/dist/components/session-list.d.ts +39 -0
  234. package/dist/components/session-mcp-capability-card.d.ts +41 -0
  235. package/dist/components/session-proxy-scope.d.ts +35 -0
  236. package/dist/components/session-status.d.ts +4 -1
  237. package/dist/components/session-usage-limit-notice.d.ts +39 -0
  238. package/dist/components/timeline-anchor.d.ts +24 -0
  239. package/dist/components/timeline-annotation-chrome.d.ts +25 -0
  240. package/dist/components/timeline-annotation-layout.d.ts +42 -0
  241. package/dist/components/timeline-annotation-markers.d.ts +8 -0
  242. package/dist/components/timeline-annotation-reveal-context.d.ts +4 -0
  243. package/dist/components/timeline-annotation-shared.d.ts +31 -0
  244. package/dist/components/timeline-annotations-dialog.d.ts +3 -2
  245. package/dist/components/timeline-annotations.d.ts +13 -3
  246. package/dist/components/timeline-reading-progress.d.ts +11 -0
  247. package/dist/components/timeline-search.d.ts +34 -0
  248. package/dist/components/timeline-settlement.d.ts +16 -0
  249. package/dist/components/tool-action-review.d.ts +47 -0
  250. package/dist/components/tool-review-history.d.ts +25 -0
  251. package/dist/components/usage-limit-notice.d.ts +49 -0
  252. package/dist/components/usage-member-list.d.ts +91 -0
  253. package/dist/components/usage-meter.d.ts +73 -0
  254. package/dist/components/use-viewer-menu-dismiss.d.ts +2 -0
  255. package/dist/components/user-message-body.d.ts +10 -1
  256. package/dist/composer-messages.d.ts +74 -0
  257. package/dist/composer-messages.js +9 -0
  258. package/dist/composer-messages.js.map +1 -0
  259. package/dist/composer-voice-events.d.ts +8 -0
  260. package/dist/composer.d.ts +5 -4
  261. package/dist/composer.js +26 -13
  262. package/dist/computer-viewer-GMLDXPON.js +20 -0
  263. package/dist/computer-viewer-GMLDXPON.js.map +1 -0
  264. package/dist/connect-account-identity.d.ts +8 -0
  265. package/dist/connect-accounts.d.ts +15 -0
  266. package/dist/connect-chooser.d.ts +18 -0
  267. package/dist/connect-panel.d.ts +18 -0
  268. package/dist/connect-setup.d.ts +18 -0
  269. package/dist/connect.d.ts +21 -0
  270. package/dist/connect.js +2149 -0
  271. package/dist/connect.js.map +1 -0
  272. package/dist/connection-catalog.d.ts +38 -0
  273. package/dist/connection-discovery.d.ts +12 -0
  274. package/dist/connection-installed.d.ts +17 -0
  275. package/dist/connection-logo.d.ts +9 -0
  276. package/dist/connection-service-logo.d.ts +8 -0
  277. package/dist/connection-service-presentation.d.ts +13 -0
  278. package/dist/connection-skeleton.d.ts +5 -0
  279. package/dist/connection-type-picker.d.ts +6 -0
  280. package/dist/conversation-timeline.d.ts +5 -0
  281. package/dist/desktop.d.ts +7 -0
  282. package/dist/desktop.js +27 -0
  283. package/dist/desktop.js.map +1 -0
  284. package/dist/device-authorization.d.ts +23 -0
  285. package/dist/diffs.d.ts +8 -0
  286. package/dist/diffs.js +15 -0
  287. package/dist/diffs.js.map +1 -0
  288. package/dist/discovery-cache.d.ts +5 -0
  289. package/dist/editor.d.ts +7 -0
  290. package/dist/editor.js +22 -0
  291. package/dist/editor.js.map +1 -0
  292. package/dist/embedded-new-voice-OCRNT6U5.js +18 -0
  293. package/dist/embedded-new-voice-OCRNT6U5.js.map +1 -0
  294. package/dist/embedded-voice-RPHPYVBT.js +18 -0
  295. package/dist/embedded-voice-RPHPYVBT.js.map +1 -0
  296. package/dist/{fleet-decision-row-GGCAT4E4.js → fleet-decision-row-SI63O5V4.js} +4 -1
  297. package/dist/fleet-decision-row-SI63O5V4.js.map +1 -0
  298. package/dist/hooks/latest-question.d.ts +25 -0
  299. package/dist/hooks/shared-machines-list.d.ts +4 -0
  300. package/dist/hooks/shared-query.d.ts +113 -0
  301. package/dist/hooks/use-attached-browsers.d.ts +1 -1
  302. package/dist/hooks/use-auth-runs.d.ts +1 -4
  303. package/dist/hooks/use-available-models.d.ts +1 -1
  304. package/dist/hooks/use-browser-frame-stream.d.ts +1 -1
  305. package/dist/hooks/use-browser-session.d.ts +12 -1
  306. package/dist/hooks/use-client-config-flags.d.ts +49 -0
  307. package/dist/hooks/use-codex-accounts.d.ts +59 -10
  308. package/dist/hooks/use-composer.d.ts +24 -2
  309. package/dist/hooks/use-computer-frame-stream.d.ts +1 -1
  310. package/dist/hooks/use-computer-input-posture.d.ts +13 -0
  311. package/dist/hooks/use-computer-session.d.ts +4 -0
  312. package/dist/hooks/use-connect.d.ts +23 -0
  313. package/dist/hooks/use-desktop-stream.d.ts +1 -1
  314. package/dist/hooks/use-file-attachments.d.ts +10 -0
  315. package/dist/hooks/use-goal.d.ts +2 -1
  316. package/dist/hooks/use-last-started-turn-policy.d.ts +5 -1
  317. package/dist/hooks/use-new-conversation.d.ts +56 -0
  318. package/dist/hooks/use-new-session-draft.d.ts +74 -0
  319. package/dist/hooks/use-realtime-voice-models.d.ts +7 -0
  320. package/dist/hooks/use-sandbox-files.d.ts +4 -1
  321. package/dist/hooks/use-session-conversation.d.ts +49 -0
  322. package/dist/hooks/use-session-events.d.ts +26 -5
  323. package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
  324. package/dist/hooks/use-session-model-account.d.ts +48 -0
  325. package/dist/hooks/use-supergrok-accounts.d.ts +7 -1
  326. package/dist/hooks/use-terminal-stream.d.ts +11 -5
  327. package/dist/hooks/use-usage.d.ts +40 -0
  328. package/dist/hooks/use-vaults.d.ts +53 -0
  329. package/dist/hooks/use-voice-input.d.ts +8 -1
  330. package/dist/hooks/use-workspace-edit.d.ts +4 -0
  331. package/dist/hooks/use-workspace-sessions.d.ts +24 -5
  332. package/dist/human-input-model.d.ts +122 -0
  333. package/dist/identity-link-accounts.d.ts +11 -0
  334. package/dist/identity-link-consent.d.ts +18 -0
  335. package/dist/index.d.ts +82 -20
  336. package/dist/index.js +2340 -6603
  337. package/dist/index.js.map +1 -1
  338. package/dist/interaction.d.ts +1 -1
  339. package/dist/interaction.js +14 -24
  340. package/dist/interaction.js.map +1 -1
  341. package/dist/latest-question-R77KWSUP.js +118 -0
  342. package/dist/latest-question-R77KWSUP.js.map +1 -0
  343. package/dist/lib/clipboard.d.ts +7 -0
  344. package/dist/lib/error-message.d.ts +7 -0
  345. package/dist/lib/format.d.ts +58 -4
  346. package/dist/lib/host-theme.d.ts +77 -0
  347. package/dist/lib/interaction-errors.d.ts +3 -0
  348. package/dist/lib/menu-styles.d.ts +22 -0
  349. package/dist/lib/notify-observer.d.ts +2 -0
  350. package/dist/lib/pierre-diffs-loader.d.ts +20 -0
  351. package/dist/lib/provider-recovery.d.ts +55 -0
  352. package/dist/lib/scrollbars.d.ts +11 -0
  353. package/dist/lib/terminal-input-readiness.d.ts +11 -0
  354. package/dist/lib/transient-retry.d.ts +25 -0
  355. package/dist/lib/workbench-peers.d.ts +36 -0
  356. package/dist/lib/xterm-theme.d.ts +7 -5
  357. package/dist/machines.js +9 -5
  358. package/dist/markdown-table-layout-3PV2UAAH.js +113 -0
  359. package/dist/markdown-table-layout-3PV2UAAH.js.map +1 -0
  360. package/dist/mcp-connection-status.d.ts +14 -0
  361. package/dist/model-mark-paths.d.ts +34 -0
  362. package/dist/model-mark-paths.js +16 -0
  363. package/dist/model-mark-paths.js.map +1 -0
  364. package/dist/model-picker-model.d.ts +111 -0
  365. package/dist/model-picker-model.js +25 -0
  366. package/dist/model-picker-model.js.map +1 -0
  367. package/dist/model-policy-picker-menu-7GJ5URPR.js +17 -0
  368. package/dist/model-policy-picker-menu-7GJ5URPR.js.map +1 -0
  369. package/dist/model-policy.d.ts +28 -2
  370. package/dist/model-policy.js +14 -1
  371. package/dist/native-previews.d.ts +28 -0
  372. package/dist/native-previews.js +82 -0
  373. package/dist/native-previews.js.map +1 -0
  374. package/dist/new-session-draft.d.ts +7 -0
  375. package/dist/new-session-draft.js +639 -0
  376. package/dist/new-session-draft.js.map +1 -0
  377. package/dist/notification-text.d.ts +27 -0
  378. package/dist/{noto-sans-arabic-loader-IA2T4X2A.js → noto-sans-arabic-loader-XSH2KYWJ.js} +3 -1
  379. package/dist/{noto-sans-arabic-loader-IA2T4X2A.js.map → noto-sans-arabic-loader-XSH2KYWJ.js.map} +1 -1
  380. package/dist/{noto-sans-jp-loader.generated-HFGLYBR3.js → noto-sans-jp-loader.generated-MR5L6V7K.js} +3 -1
  381. package/dist/{noto-sans-jp-loader.generated-HFGLYBR3.js.map → noto-sans-jp-loader.generated-MR5L6V7K.js.map} +1 -1
  382. package/dist/older-history.d.ts +3 -2
  383. package/dist/organization-model.d.ts +32 -0
  384. package/dist/organization-model.js +65 -0
  385. package/dist/organization-model.js.map +1 -0
  386. package/dist/{platform-activity-row-BJHUHXAT.js → platform-activity-row-5OCKOQWZ.js} +16 -78
  387. package/dist/platform-activity-row-5OCKOQWZ.js.map +1 -0
  388. package/dist/plugin-details.d.ts +14 -0
  389. package/dist/plugin-discovery.d.ts +25 -0
  390. package/dist/preview-loading-paint.d.ts +20 -0
  391. package/dist/provider.d.ts +25 -2
  392. package/dist/queue-presentation-model.d.ts +72 -0
  393. package/dist/queue-presentation-model.js +38 -0
  394. package/dist/queue-presentation-model.js.map +1 -0
  395. package/dist/{queue-surface-implementation-BSKUWHRI.js → queue-surface-implementation-GJBH7NKO.js} +65 -265
  396. package/dist/queue-surface-implementation-GJBH7NKO.js.map +1 -0
  397. package/dist/realtime/embedded-new-voice.d.ts +1 -0
  398. package/dist/realtime/embedded-voice.d.ts +1 -0
  399. package/dist/realtime/realtime-control.d.ts +4 -65
  400. package/dist/realtime/session-realtime.d.ts +78 -0
  401. package/dist/realtime.js +25 -1333
  402. package/dist/realtime.js.map +1 -1
  403. package/dist/sandbox-files-O27IPOZW.js +21 -0
  404. package/dist/sandbox-files-O27IPOZW.js.map +1 -0
  405. package/dist/sandbox-label-model.d.ts +30 -0
  406. package/dist/sandbox-label-model.js +20 -0
  407. package/dist/sandbox-label-model.js.map +1 -0
  408. package/dist/scrollbars.d.ts +1 -0
  409. package/dist/scrollbars.js +10 -0
  410. package/dist/scrollbars.js.map +1 -0
  411. package/dist/secret-reference-label.d.ts +14 -0
  412. package/dist/secrets-ui.d.ts +57 -0
  413. package/dist/session-agents-model.d.ts +10 -0
  414. package/dist/session-agents-model.js +25 -0
  415. package/dist/session-agents-model.js.map +1 -0
  416. package/dist/session-attention-model.d.ts +8 -0
  417. package/dist/session-attention-model.js +47 -0
  418. package/dist/session-attention-model.js.map +1 -0
  419. package/dist/session-auth-recommendation.d.ts +5 -0
  420. package/dist/session-capability-policy.d.ts +31 -0
  421. package/dist/session-feedback-model.d.ts +20 -0
  422. package/dist/session-feedback-model.js +56 -0
  423. package/dist/session-feedback-model.js.map +1 -0
  424. package/dist/session-list-model.d.ts +138 -0
  425. package/dist/session-list-model.js +274 -0
  426. package/dist/session-list-model.js.map +1 -0
  427. package/dist/session-realtime-7DJOIHQW.js +21 -0
  428. package/dist/session-realtime-7DJOIHQW.js.map +1 -0
  429. package/dist/session-realtime.d.ts +7 -0
  430. package/dist/session-realtime.js +19 -0
  431. package/dist/session-realtime.js.map +1 -0
  432. package/dist/session-status-model.d.ts +36 -0
  433. package/dist/session-ui.d.ts +39 -1
  434. package/dist/session-ui.js +126 -12
  435. package/dist/session-ui.js.map +1 -1
  436. package/dist/session.d.ts +8 -2
  437. package/dist/session.js +35 -20
  438. package/dist/site-snapshot.d.ts +14 -0
  439. package/dist/sites-ui.d.ts +24 -0
  440. package/dist/sites.d.ts +3 -0
  441. package/dist/sites.js +14 -0
  442. package/dist/sites.js.map +1 -0
  443. package/dist/skill-discovery.d.ts +47 -0
  444. package/dist/terminal.d.ts +8 -0
  445. package/dist/terminal.js +32 -0
  446. package/dist/terminal.js.map +1 -0
  447. package/dist/testing/session-scenarios.d.ts +10 -0
  448. package/dist/testing/session-scenarios.js +367 -0
  449. package/dist/testing/session-scenarios.js.map +1 -0
  450. package/dist/timeline/activity-rail.d.ts +11 -2
  451. package/dist/timeline/agent-identity.d.ts +30 -0
  452. package/dist/timeline/agent-row.d.ts +31 -0
  453. package/dist/timeline/allowance-exhausted-row.d.ts +24 -0
  454. package/dist/timeline/compaction-copy.d.ts +46 -0
  455. package/dist/timeline/focus-sequence.d.ts +9 -0
  456. package/dist/timeline/genie-copy.d.ts +3 -0
  457. package/dist/timeline/genie-loading.d.ts +47 -0
  458. package/dist/timeline/index.d.ts +10 -5
  459. package/dist/timeline/knowledge-receipt.d.ts +17 -0
  460. package/dist/timeline/message-sender.d.ts +19 -0
  461. package/dist/timeline/notice-presentation.d.ts +13 -0
  462. package/dist/timeline/parsers.d.ts +11 -0
  463. package/dist/timeline/patch-apply-command.d.ts +18 -0
  464. package/dist/timeline/platform-activity-presentation.d.ts +21 -0
  465. package/dist/timeline/platform-activity-row.d.ts +1 -2
  466. package/dist/timeline/presented-image.d.ts +11 -0
  467. package/dist/timeline/projection.d.ts +31 -2
  468. package/dist/timeline/provider-logo.d.ts +15 -0
  469. package/dist/timeline/question-nav-model.d.ts +23 -0
  470. package/dist/timeline/registry.d.ts +12 -1
  471. package/dist/timeline/retained-image.d.ts +18 -0
  472. package/dist/timeline/retained-loaders.d.ts +27 -0
  473. package/dist/timeline/rolling-activity.d.ts +11 -0
  474. package/dist/timeline/screenshot-lightbox.d.ts +23 -1
  475. package/dist/timeline/session-usage-limit-row.d.ts +11 -0
  476. package/dist/timeline/shared.d.ts +4 -1
  477. package/dist/timeline/startup-dispatch-details.d.ts +5 -0
  478. package/dist/timeline/startup-preference.d.ts +3 -0
  479. package/dist/timeline/startup-timings.d.ts +4 -0
  480. package/dist/timeline/tool-display-name.d.ts +5 -2
  481. package/dist/timeline/tool-presentation.d.ts +172 -0
  482. package/dist/timeline/turn-summary-model.d.ts +95 -0
  483. package/dist/timeline/turn-summary.d.ts +20 -54
  484. package/dist/timeline/types.d.ts +179 -7
  485. package/dist/timeline/work-presentation.d.ts +60 -0
  486. package/dist/timeline-annotation-markers-U3G66KUD.js +166 -0
  487. package/dist/timeline-annotation-markers-U3G66KUD.js.map +1 -0
  488. package/dist/timeline-annotation-selection-BI7QTPTZ.js +242 -0
  489. package/dist/timeline-annotation-selection-BI7QTPTZ.js.map +1 -0
  490. package/dist/timeline-annotations-dialog-FOMHSDTE.js +274 -0
  491. package/dist/timeline-annotations-dialog-FOMHSDTE.js.map +1 -0
  492. package/dist/timeline-model.d.ts +29 -0
  493. package/dist/timeline-model.js +254 -0
  494. package/dist/timeline-model.js.map +1 -0
  495. package/dist/usage/allowance-copy.d.ts +25 -0
  496. package/dist/usage/session-usage-limit.d.ts +88 -0
  497. package/dist/usage/summary.d.ts +35 -0
  498. package/dist/usage.d.ts +17 -0
  499. package/dist/usage.js +1187 -0
  500. package/dist/usage.js.map +1 -0
  501. package/dist/voice-recording-owner.d.ts +7 -0
  502. package/package.json +106 -7
  503. package/src/approvals.ts +8 -1
  504. package/src/artifacts-spreadsheet.ts +1 -0
  505. package/src/artifacts.ts +44 -0
  506. package/src/assets/README.md +8 -0
  507. package/src/assets/gmail.ico +0 -0
  508. package/src/assets/outlook.ico +0 -0
  509. package/src/capability-catalog-row.tsx +94 -0
  510. package/src/capability-logo-fallback.ts +22 -0
  511. package/src/client.ts +20 -31
  512. package/src/commands/registry.ts +18 -15
  513. package/src/commands/types.ts +2 -0
  514. package/src/components/anthropic-mark.tsx +10 -0
  515. package/src/components/approval-surface.tsx +401 -138
  516. package/src/components/artifacts/artifact-chrome.tsx +413 -0
  517. package/src/components/artifacts/artifact-sandbox.tsx +193 -0
  518. package/src/components/artifacts/artifact-surface.tsx +40 -20
  519. package/src/components/artifacts/chat-interactive-block.tsx +260 -0
  520. package/src/components/artifacts/deferred-chat-media.tsx +67 -0
  521. package/src/components/artifacts/document-editor.tsx +141 -16
  522. package/src/components/artifacts/editable-artifact-ui.tsx +4 -2
  523. package/src/components/artifacts/editable-artifact-view.tsx +197 -0
  524. package/src/components/artifacts/editable-artifact-workbench.tsx +5 -2
  525. package/src/components/artifacts/editable-document.tsx +37 -36
  526. package/src/components/artifacts/editable-presentation.tsx +36 -18
  527. package/src/components/artifacts/editable-spreadsheet.tsx +543 -103
  528. package/src/components/artifacts/index.ts +45 -0
  529. package/src/components/artifacts/inline-html-document.ts +24 -0
  530. package/src/components/artifacts/latest-task-runner.ts +49 -0
  531. package/src/components/artifacts/presentation-editor.tsx +323 -67
  532. package/src/components/artifacts/published-html-artifact-frame.tsx +122 -5
  533. package/src/components/artifacts/session-artifact-viewer.tsx +378 -0
  534. package/src/components/artifacts/site-view.tsx +121 -0
  535. package/src/components/artifacts/spreadsheet-canvas.ts +5 -10
  536. package/src/components/artifacts/spreadsheet-command-queue.ts +184 -0
  537. package/src/components/artifacts/spreadsheet-download.tsx +109 -0
  538. package/src/components/artifacts/spreadsheet-general-display.ts +43 -0
  539. package/src/components/artifacts/spreadsheet-grid.tsx +344 -144
  540. package/src/components/artifacts/spreadsheet-resize.tsx +421 -0
  541. package/src/components/artifacts/spreadsheet-submissions.tsx +46 -0
  542. package/src/components/artifacts/visualization-assets/base.css +1015 -0
  543. package/src/components/artifacts/visualization-assets/helpers.html +349 -0
  544. package/src/components/artifacts/visualization-assets.gen.ts +3 -0
  545. package/src/components/browser-select-control.tsx +312 -0
  546. package/src/components/browser-viewer.tsx +778 -318
  547. package/src/components/chat-composer.tsx +47 -5
  548. package/src/components/child-session-link.tsx +36 -0
  549. package/src/components/claude-mark.tsx +11 -0
  550. package/src/components/code-editor.tsx +13 -16
  551. package/src/components/composer-transcription-control.tsx +128 -30
  552. package/src/components/composer.tsx +390 -191
  553. package/src/components/computer-viewer.tsx +880 -253
  554. package/src/components/copy-button.tsx +13 -5
  555. package/src/components/desktop-viewer.tsx +1 -1
  556. package/src/components/enrollment-consent.tsx +1 -1
  557. package/src/components/enrollment-device-flow.tsx +1 -1
  558. package/src/components/file-browser.tsx +10 -14
  559. package/src/components/fleet-tile.tsx +5 -3
  560. package/src/components/generated-video-player.tsx +4 -2
  561. package/src/components/grok-mark.tsx +34 -0
  562. package/src/components/human-input-form.tsx +590 -283
  563. package/src/components/human-input-surface.tsx +11 -0
  564. package/src/components/interaction-intervention-banner.tsx +2 -2
  565. package/src/components/linkified-text.tsx +79 -0
  566. package/src/components/machine-card.tsx +35 -78
  567. package/src/components/machine-dock-bar.tsx +1 -1
  568. package/src/components/machine-input-display.ts +197 -3
  569. package/src/components/machines/agent-status-icon.tsx +31 -0
  570. package/src/components/machines/machine-detail.tsx +200 -31
  571. package/src/components/machines/machine-operation-policy.tsx +1 -1
  572. package/src/components/machines/machine-status.ts +241 -0
  573. package/src/components/machines-dashboard.tsx +2 -17
  574. package/src/components/markdown-table-layout.ts +143 -0
  575. package/src/components/markdown.tsx +466 -129
  576. package/src/components/message-timeline.tsx +2370 -667
  577. package/src/components/model-mark.tsx +102 -0
  578. package/src/components/model-picker.tsx +15 -4
  579. package/src/components/model-policy-picker-menu.tsx +371 -0
  580. package/src/components/model-policy-picker.tsx +252 -454
  581. package/src/components/new-conversation.tsx +221 -0
  582. package/src/components/open-geni-chat.tsx +309 -0
  583. package/src/components/open-geni-links.tsx +179 -0
  584. package/src/components/openrouter-mark.tsx +13 -0
  585. package/src/components/opper-mark.tsx +18 -0
  586. package/src/components/pierre-diff.tsx +41 -16
  587. package/src/components/pierre-file.tsx +35 -10
  588. package/src/components/preview-loading.tsx +109 -0
  589. package/src/components/provider-recovery-notice.tsx +49 -0
  590. package/src/components/queue-item-content.ts +14 -0
  591. package/src/components/queue-surface-implementation.tsx +51 -356
  592. package/src/components/queue-surface-state.tsx +3 -1
  593. package/src/components/rehype-secret-refs.ts +98 -0
  594. package/src/components/remark-soft-line-breaks.ts +166 -0
  595. package/src/components/sandbox-files.tsx +91 -15
  596. package/src/components/sandbox-terminal.tsx +84 -13
  597. package/src/components/sandbox-workspace.tsx +147 -51
  598. package/src/components/secret-question-field.tsx +324 -0
  599. package/src/components/secret-ref-chip.tsx +185 -0
  600. package/src/components/session-capability-frame.tsx +197 -0
  601. package/src/components/session-chrome.tsx +842 -251
  602. package/src/components/session-commands-panel.tsx +126 -0
  603. package/src/components/session-commands.tsx +18 -0
  604. package/src/components/session-connection-request.tsx +97 -0
  605. package/src/components/session-conversation.tsx +876 -0
  606. package/src/components/session-list.tsx +257 -0
  607. package/src/components/session-mcp-capability-card.tsx +680 -0
  608. package/src/components/session-proxy-scope.tsx +153 -0
  609. package/src/components/session-status.tsx +26 -17
  610. package/src/components/session-usage-limit-notice.tsx +174 -0
  611. package/src/components/timeline-anchor.tsx +119 -0
  612. package/src/components/timeline-annotation-chrome.tsx +196 -0
  613. package/src/components/timeline-annotation-layout.ts +268 -0
  614. package/src/components/timeline-annotation-markers.tsx +185 -0
  615. package/src/components/timeline-annotation-reveal-context.ts +9 -0
  616. package/src/components/timeline-annotation-selection.tsx +106 -54
  617. package/src/components/timeline-annotation-shared.ts +245 -0
  618. package/src/components/timeline-annotations-dialog.tsx +185 -128
  619. package/src/components/timeline-annotations.tsx +189 -45
  620. package/src/components/timeline-reading-progress.ts +80 -0
  621. package/src/components/timeline-search.tsx +168 -0
  622. package/src/components/timeline-settlement.ts +60 -0
  623. package/src/components/tool-action-review.tsx +650 -0
  624. package/src/components/tool-review-history.tsx +88 -0
  625. package/src/components/usage-limit-notice.tsx +179 -0
  626. package/src/components/usage-member-list.tsx +792 -0
  627. package/src/components/usage-meter.tsx +382 -0
  628. package/src/components/use-viewer-menu-dismiss.ts +33 -0
  629. package/src/components/user-message-body.tsx +27 -2
  630. package/src/components/workspace-dock.tsx +18 -1
  631. package/src/composer-messages.ts +150 -0
  632. package/src/composer-voice-events.ts +12 -0
  633. package/src/composer.ts +6 -1
  634. package/src/connect-account-identity.tsx +34 -0
  635. package/src/connect-accounts.tsx +233 -0
  636. package/src/connect-chooser.tsx +336 -0
  637. package/src/connect-panel.tsx +99 -0
  638. package/src/connect-setup.tsx +355 -0
  639. package/src/connect.ts +60 -0
  640. package/src/connection-assets.d.ts +4 -0
  641. package/src/connection-catalog.tsx +162 -0
  642. package/src/connection-discovery.tsx +238 -0
  643. package/src/connection-installed.tsx +51 -0
  644. package/src/connection-logo.tsx +37 -0
  645. package/src/connection-service-logo.tsx +55 -0
  646. package/src/connection-service-presentation.ts +46 -0
  647. package/src/connection-skeleton.tsx +16 -0
  648. package/src/connection-type-picker.tsx +36 -0
  649. package/src/conversation-timeline.ts +92 -0
  650. package/src/desktop.ts +24 -0
  651. package/src/device-authorization.tsx +98 -0
  652. package/src/diffs.ts +16 -0
  653. package/src/discovery-cache.ts +42 -0
  654. package/src/editor.ts +13 -0
  655. package/src/hooks/internal.ts +3 -2
  656. package/src/hooks/latest-question.ts +164 -0
  657. package/src/hooks/shared-machines-list.ts +34 -1
  658. package/src/hooks/shared-query.ts +721 -0
  659. package/src/hooks/use-attached-browsers.ts +1 -1
  660. package/src/hooks/use-auth-runs.ts +0 -27
  661. package/src/hooks/use-available-models.ts +2 -2
  662. package/src/hooks/use-browser-frame-stream.ts +19 -2
  663. package/src/hooks/use-browser-session.ts +680 -205
  664. package/src/hooks/use-browser-sessions.ts +32 -1
  665. package/src/hooks/use-client-config-flags.ts +118 -0
  666. package/src/hooks/use-codex-accounts.ts +247 -47
  667. package/src/hooks/use-composer.ts +227 -44
  668. package/src/hooks/use-computer-frame-stream.ts +25 -7
  669. package/src/hooks/use-computer-input-posture.ts +94 -0
  670. package/src/hooks/use-computer-session.ts +409 -95
  671. package/src/hooks/use-connect.ts +23 -0
  672. package/src/hooks/use-desktop-stream.ts +19 -28
  673. package/src/hooks/use-file-attachments.ts +53 -3
  674. package/src/hooks/use-goal.ts +79 -9
  675. package/src/hooks/use-last-started-turn-policy.ts +37 -11
  676. package/src/hooks/use-machines.ts +4 -0
  677. package/src/hooks/use-new-conversation.ts +374 -0
  678. package/src/hooks/use-new-session-draft.ts +813 -0
  679. package/src/hooks/use-realtime-voice-models.ts +46 -0
  680. package/src/hooks/use-sandbox-files.ts +68 -20
  681. package/src/hooks/use-session-background-commands.ts +22 -11
  682. package/src/hooks/use-session-capabilities.ts +3 -2
  683. package/src/hooks/use-session-conversation.ts +150 -0
  684. package/src/hooks/use-session-events.ts +531 -335
  685. package/src/hooks/use-session-mcp-approval-policy.ts +13 -5
  686. package/src/hooks/use-session-model-account.ts +174 -0
  687. package/src/hooks/use-session.ts +30 -2
  688. package/src/hooks/use-slash-commands.ts +6 -11
  689. package/src/hooks/use-supergrok-accounts.ts +22 -8
  690. package/src/hooks/use-terminal-stream.ts +153 -27
  691. package/src/hooks/use-transcription.ts +6 -0
  692. package/src/hooks/use-turn-queue.ts +2 -1
  693. package/src/hooks/use-usage.ts +100 -0
  694. package/src/hooks/use-vaults.ts +153 -0
  695. package/src/hooks/use-video-artifact-playback.ts +1 -1
  696. package/src/hooks/use-voice-input.ts +127 -19
  697. package/src/hooks/use-workspace-capture.ts +3 -2
  698. package/src/hooks/use-workspace-edit.ts +23 -5
  699. package/src/hooks/use-workspace-sessions.ts +74 -7
  700. package/src/human-input-model.ts +359 -0
  701. package/src/identity-link-accounts.tsx +148 -0
  702. package/src/identity-link-consent.tsx +192 -0
  703. package/src/index.ts +262 -16
  704. package/src/interaction.ts +1 -0
  705. package/src/lib/clipboard.ts +29 -0
  706. package/src/lib/error-message.ts +37 -0
  707. package/src/lib/format.ts +205 -10
  708. package/src/lib/host-theme.ts +529 -0
  709. package/src/lib/interaction-errors.ts +15 -0
  710. package/src/lib/menu-styles.ts +44 -0
  711. package/src/lib/notify-observer.ts +15 -0
  712. package/src/lib/pierre-diffs-loader.ts +53 -0
  713. package/src/lib/provider-recovery.ts +243 -0
  714. package/src/lib/scrollbars.ts +30 -0
  715. package/src/lib/terminal-input-readiness.ts +51 -0
  716. package/src/lib/transient-retry.ts +99 -0
  717. package/src/lib/workbench-peers.ts +62 -0
  718. package/src/lib/xterm-theme.ts +23 -21
  719. package/src/mcp-connection-status.ts +35 -0
  720. package/src/model-mark-paths.ts +53 -0
  721. package/src/model-picker-model.ts +330 -0
  722. package/src/model-policy.ts +261 -27
  723. package/src/native-previews.ts +100 -0
  724. package/src/new-session-draft.ts +22 -0
  725. package/src/notification-text.ts +81 -0
  726. package/src/older-history.ts +22 -4
  727. package/src/organization-model.ts +118 -0
  728. package/src/plugin-details.tsx +185 -0
  729. package/src/plugin-discovery.tsx +196 -0
  730. package/src/preview-loading-paint.ts +68 -0
  731. package/src/provider.tsx +74 -15
  732. package/src/queue-presentation-model.ts +464 -0
  733. package/src/realtime/dropdown-menu.tsx +19 -12
  734. package/src/realtime/embedded-new-voice.tsx +1 -0
  735. package/src/realtime/embedded-voice.tsx +3 -0
  736. package/src/realtime/realtime-control.tsx +138 -550
  737. package/src/realtime/session-realtime.ts +566 -0
  738. package/src/sandbox-label-model.ts +53 -0
  739. package/src/scrollbars.ts +3 -0
  740. package/src/secret-reference-label.ts +43 -0
  741. package/src/secrets-ui.tsx +59 -0
  742. package/src/session-agents-model.ts +33 -0
  743. package/src/session-attention-model.ts +62 -0
  744. package/src/session-auth-recommendation.ts +48 -0
  745. package/src/session-capability-policy.ts +196 -0
  746. package/src/session-context.ts +3 -4
  747. package/src/session-feedback-model.ts +95 -0
  748. package/src/session-list-model.ts +434 -0
  749. package/src/session-realtime.ts +18 -0
  750. package/src/session-status-model.ts +99 -0
  751. package/src/session-ui.ts +98 -1
  752. package/src/session.ts +17 -1
  753. package/src/site-snapshot.ts +74 -0
  754. package/src/sites-ui.tsx +348 -0
  755. package/src/sites.ts +3 -0
  756. package/src/skill-discovery.tsx +208 -0
  757. package/src/terminal.ts +26 -0
  758. package/src/testing/session-scenarios.ts +404 -0
  759. package/src/timeline/activity-rail.tsx +242 -101
  760. package/src/timeline/agent-identity.tsx +84 -0
  761. package/src/timeline/agent-row.tsx +231 -0
  762. package/src/timeline/allowance-exhausted-row.tsx +86 -0
  763. package/src/timeline/compaction-copy.ts +115 -0
  764. package/src/timeline/fleet-decision-projection.ts +2 -1
  765. package/src/timeline/fleet-decision-row.tsx +1 -0
  766. package/src/timeline/focus-sequence.ts +44 -0
  767. package/src/timeline/genie-copy.ts +19 -0
  768. package/src/timeline/genie-loading.tsx +143 -0
  769. package/src/timeline/index.ts +30 -2
  770. package/src/timeline/knowledge-receipt.tsx +100 -0
  771. package/src/timeline/message-sender.tsx +71 -0
  772. package/src/timeline/notice-presentation.ts +48 -0
  773. package/src/timeline/parsers.ts +42 -2
  774. package/src/timeline/patch-apply-command.tsx +135 -0
  775. package/src/timeline/platform-activity-presentation.ts +166 -0
  776. package/src/timeline/platform-activity-row.tsx +15 -90
  777. package/src/timeline/presented-image.ts +63 -0
  778. package/src/timeline/projection.ts +1742 -76
  779. package/src/timeline/provider-logo.tsx +53 -0
  780. package/src/timeline/question-nav-model.ts +28 -0
  781. package/src/timeline/registry.ts +26 -4
  782. package/src/timeline/retained-image.ts +97 -0
  783. package/src/timeline/retained-loaders.ts +91 -0
  784. package/src/timeline/rolling-activity.tsx +80 -0
  785. package/src/timeline/screenshot-lightbox.tsx +228 -5
  786. package/src/timeline/session-usage-limit-row.tsx +42 -0
  787. package/src/timeline/shared.tsx +42 -15
  788. package/src/timeline/startup-dispatch-details.tsx +36 -0
  789. package/src/timeline/startup-preference.ts +33 -0
  790. package/src/timeline/startup-timings.tsx +145 -0
  791. package/src/timeline/tool-display-name.ts +19 -7
  792. package/src/timeline/tool-presentation.ts +1316 -0
  793. package/src/timeline/tool-renderers.tsx +835 -1138
  794. package/src/timeline/turn-summary-model.ts +343 -0
  795. package/src/timeline/turn-summary.tsx +272 -365
  796. package/src/timeline/types.ts +189 -6
  797. package/src/timeline/work-presentation.ts +189 -0
  798. package/src/timeline-model.ts +143 -0
  799. package/src/usage/allowance-copy.ts +64 -0
  800. package/src/usage/session-usage-limit.ts +266 -0
  801. package/src/usage/summary.ts +93 -0
  802. package/src/usage.ts +70 -0
  803. package/src/voice-recording-owner.ts +22 -0
  804. package/styles/compiled.css +2318 -752
  805. package/styles/connect.css +290 -0
  806. package/styles/connect.d.ts +1 -0
  807. package/styles/effective-tokens.css +1 -0
  808. package/styles/index.css +309 -9
  809. package/styles/responsive.css +99 -7
  810. package/styles/session-capability.css +44 -0
  811. package/styles/tokens.css +134 -66
  812. package/dist/browser-viewer-MKKFRRRV.js +0 -14
  813. package/dist/chunk-4MDLEHXL.js.map +0 -1
  814. package/dist/chunk-5YK2ZDIG.js.map +0 -1
  815. package/dist/chunk-7EAFGICR.js +0 -302
  816. package/dist/chunk-7EAFGICR.js.map +0 -1
  817. package/dist/chunk-7KBJLTKS.js +0 -494
  818. package/dist/chunk-7KBJLTKS.js.map +0 -1
  819. package/dist/chunk-E5KISKFG.js.map +0 -1
  820. package/dist/chunk-ELIYWBZR.js.map +0 -1
  821. package/dist/chunk-H5YGYNXS.js.map +0 -1
  822. package/dist/chunk-HUIKIDM5.js.map +0 -1
  823. package/dist/chunk-ICA6R6TW.js.map +0 -1
  824. package/dist/chunk-IOLVN74D.js.map +0 -1
  825. package/dist/chunk-JAQ5K7W6.js.map +0 -1
  826. package/dist/chunk-JVYQRRC4.js.map +0 -1
  827. package/dist/chunk-KCGXPP5V.js.map +0 -1
  828. package/dist/chunk-KZ73RL76.js.map +0 -1
  829. package/dist/chunk-NEXLLTP4.js.map +0 -1
  830. package/dist/chunk-PEI6RANI.js.map +0 -1
  831. package/dist/chunk-VTOREEXF.js +0 -142
  832. package/dist/chunk-VTOREEXF.js.map +0 -1
  833. package/dist/chunk-WSK7G5LE.js +0 -170
  834. package/dist/chunk-WSK7G5LE.js.map +0 -1
  835. package/dist/chunk-X5S426G2.js +0 -217
  836. package/dist/chunk-X5S426G2.js.map +0 -1
  837. package/dist/chunk-YG4QEQF5.js +0 -9557
  838. package/dist/chunk-YG4QEQF5.js.map +0 -1
  839. package/dist/chunk-YVCHAQZY.js.map +0 -1
  840. package/dist/chunk-ZKNTK7IA.js.map +0 -1
  841. package/dist/chunk-ZPBTI3TE.js.map +0 -1
  842. package/dist/computer-viewer-FWEZISW4.js +0 -16
  843. package/dist/fleet-decision-row-GGCAT4E4.js.map +0 -1
  844. package/dist/hooks/use-environments.d.ts +0 -41
  845. package/dist/hooks/use-packs.d.ts +0 -32
  846. package/dist/platform-activity-row-BJHUHXAT.js.map +0 -1
  847. package/dist/queue-surface-implementation-BSKUWHRI.js.map +0 -1
  848. package/dist/timeline-annotation-selection-3L7LHGG2.js +0 -187
  849. package/dist/timeline-annotation-selection-3L7LHGG2.js.map +0 -1
  850. package/dist/timeline-annotations-dialog-U7EVLR75.js +0 -196
  851. package/dist/timeline-annotations-dialog-U7EVLR75.js.map +0 -1
  852. package/src/hooks/use-environments.ts +0 -182
  853. package/src/hooks/use-packs.ts +0 -157
  854. /package/dist/{browser-viewer-MKKFRRRV.js.map → browser-viewer-ZWR6MWJ7.js.map} +0 -0
  855. /package/dist/{computer-viewer-FWEZISW4.js.map → chat-interactive-block-4EUKJXUO.js.map} +0 -0
@@ -1,3 +1,35 @@
1
+ import { QUESTION_NAV_MARGIN_PX, questionNavTarget } from "../timeline/question-nav-model";
2
+ import { timelineGroupIndexAtSequence } from "../timeline/focus-sequence";
3
+ import { RollingActivity } from "../timeline/rolling-activity";
4
+ import {
5
+ clusterIsSettled,
6
+ compactedLandmarkCount,
7
+ durationBetween,
8
+ flattenActivityItems,
9
+ isPreparingWork,
10
+ readableWorkDefaultOpen,
11
+ readableWorkHasDetails,
12
+ readableWorkShowsPreview,
13
+ readableWorkStatus,
14
+ } from "../timeline/work-presentation";
15
+ import { useErrorMessage } from "../lib/error-message";
16
+ import { formatElapsed } from "../timeline/turn-summary";
17
+ import { ActivityNoteTextContext } from "../timeline/activity-rail";
18
+ import { timelineGroupContainsPresentedImage } from "../timeline/presented-image";
19
+ import {
20
+ isTimelineSearchTarget,
21
+ TimelineSearchRevealContext,
22
+ useTimelineSearchNavigation,
23
+ type TimelineSearchTarget,
24
+ } from "./timeline-search";
25
+ import { GenieLoadingOptionsContext, type GenieLoadingOptions } from "../timeline/genie-loading";
26
+ import { ChildSessionLink } from "./child-session-link";
27
+ import { useStartupDetails } from "../timeline/startup-preference";
28
+ import {
29
+ compactionFailureDetails,
30
+ compactionSkipSubtitle,
31
+ compactionSkipTitle,
32
+ } from "../timeline/compaction-copy";
1
33
  import type {
2
34
  DraftTimelineAnnotation,
3
35
  MediaGenerationResult,
@@ -8,10 +40,13 @@ import { dequal } from "dequal/lite";
8
40
  import {
9
41
  ArrowDownIcon,
10
42
  ArrowRightIcon,
43
+ ArrowUpIcon,
11
44
  BotIcon,
12
45
  CheckCircle2Icon,
13
46
  CheckIcon,
47
+ ChevronDownIcon,
14
48
  ChevronRightIcon,
49
+ CornerDownRightIcon,
15
50
  PauseCircleIcon,
16
51
  PauseIcon,
17
52
  MessageCircleQuestionIcon,
@@ -24,16 +59,19 @@ import {
24
59
  TriangleAlertIcon,
25
60
  XCircleIcon,
26
61
  } from "lucide-react";
27
- import type { ComponentType } from "react";
28
- import { AnimatePresence, motion } from "motion/react";
29
- import { Collapsible } from "radix-ui";
62
+ import type { ComponentType, CSSProperties } from "react";
63
+ import { AnimatePresence, motion, useReducedMotion } from "motion/react";
30
64
  import {
31
65
  Component,
66
+ Fragment,
32
67
  Suspense,
68
+ createContext,
33
69
  lazy,
34
70
  memo,
35
71
  useCallback,
72
+ useContext,
36
73
  useEffect,
74
+ useId,
37
75
  useLayoutEffect,
38
76
  useMemo,
39
77
  useRef,
@@ -41,6 +79,11 @@ import {
41
79
  type ReactNode,
42
80
  } from "react";
43
81
  import { cn } from "../lib/cn";
82
+ import {
83
+ MessageSenderContext,
84
+ MessageSenderSlot,
85
+ type RenderMessageSender,
86
+ } from "../timeline/message-sender";
44
87
  import { formatClockTime, formatRelativeTime, truncate } from "../lib/format";
45
88
  import { prefersReducedMotion } from "../lib/motion";
46
89
  import {
@@ -48,11 +91,27 @@ import {
48
91
  type OlderHistoryLoadReceipt,
49
92
  type OlderHistoryLoader,
50
93
  } from "../older-history";
51
- import { Markdown } from "./markdown";
94
+ import { Markdown, type MarkdownProps } from "./markdown";
95
+ import { linkifyText } from "./linkified-text";
96
+ import { OpenGeniLinkProvider, type OpenGeniLinkResolver } from "./open-geni-links";
97
+ import {
98
+ TimelineBeforeLayout,
99
+ captureTimelineAnchor,
100
+ timelineAnchorCorrection,
101
+ timelineHasReader,
102
+ type TimelineAnchor,
103
+ } from "./timeline-anchor";
104
+ import { useReadingProgress } from "./timeline-reading-progress";
105
+ import {
106
+ animateTimelineSettlement,
107
+ captureTimelineSettlement,
108
+ type TimelineSettlement,
109
+ } from "./timeline-settlement";
52
110
  import {
53
111
  UserMessageBody,
54
112
  UserMessageDisclosureProvider,
55
113
  type UserMessageDisclosureContextValue,
114
+ type UserMessageDisclosureLabels,
56
115
  } from "./user-message-body";
57
116
  import {
58
117
  createTipFollowState,
@@ -89,6 +148,7 @@ import {
89
148
  type VideoArtifactPlaybackLoader,
90
149
  type ToolRegistry,
91
150
  type TurnSummaryOptions,
151
+ type TurnSummaryStatus,
92
152
  type UserMessageItem,
93
153
  type FoldRestingState,
94
154
  type WorkerCompletionItem,
@@ -99,42 +159,104 @@ import {
99
159
  useTurnSettleOpen,
100
160
  } from "../timeline";
101
161
  import { CopyHoverFrame } from "./copy-button";
162
+ import { SecretAnswerChip } from "./secret-ref-chip";
163
+ import { secretAnswerLabel } from "../human-input-model";
164
+ import { TimelineAnnotationSourceRootContext } from "./timeline-annotation-reveal-context";
102
165
  import { GeneratedVideoPlayer } from "./generated-video-player";
103
166
  import {
104
167
  MACHINE_INPUT_META,
168
+ agentMemberSessionId,
169
+ agentMemberText,
170
+ agentMemberTitle,
105
171
  cleanMachineInputSummary,
172
+ isAgentMember,
106
173
  machineInputBatchLabel,
107
174
  machineInputSummaryIsUseful,
108
175
  readableMachineInputSource,
109
176
  } from "./machine-input-display";
177
+ import { AgentRow, AgentRowSection, AgentRowText } from "../timeline/agent-row";
178
+ import {
179
+ AgentIdentityProvider,
180
+ useAgentIdentity,
181
+ type ResolveSessionTitle,
182
+ } from "../timeline/agent-identity";
183
+ import { namedAgentTitle, type AgentTitleParts } from "../timeline/platform-activity-presentation";
110
184
  import { SESSION_STATUS_META, StatusDot } from "./session-status";
185
+ import {
186
+ noticeDisplayText,
187
+ noticeIsResolvedApproval,
188
+ noticeTone,
189
+ } from "../timeline/notice-presentation";
111
190
  import { TimelineComputeLabelProvider } from "../timeline/compute-label";
191
+ import { ProviderLogoProvider } from "../timeline/provider-logo";
112
192
  import { EntranceAnimationProvider, useEntranceAnimation } from "../timeline/entrance";
193
+ import {
194
+ AllowanceExhaustedRow,
195
+ AllowancePresentationProvider,
196
+ type RenderAllowanceExhausted,
197
+ } from "../timeline/allowance-exhausted-row";
198
+ import { SessionUsageLimitRow } from "../timeline/session-usage-limit-row";
199
+ import type { AllowanceLabels } from "../usage/allowance-copy";
113
200
  import { SeenActivityIdsProvider } from "../timeline/seen-activity-ids";
114
- import { TimelineAnnotationsChip } from "./timeline-annotations";
201
+ import { TimelineAnnotationCards } from "./timeline-annotations";
115
202
  import { TooltipProvider } from "./tooltip";
116
203
 
117
204
  const TimelineAnnotationSelection = lazy(() => import("./timeline-annotation-selection"));
205
+ const TimelineAnnotationMarkers = lazy(() => import("./timeline-annotation-markers"));
118
206
 
119
207
  export type MessageTimelineProps = {
208
+ /** Exact durable search hit; clearing it removes highlighting without moving the reader. */
209
+ searchTarget?: TimelineSearchTarget | null | undefined;
210
+ /**
211
+ * A durable event sequence to open on once (an inbox item's or notification's
212
+ * moment): the timeline lands on the row that holds it, or the nearest row
213
+ * before it, instead of the latest message. Load the window around it first.
214
+ */
215
+ focusSequence?: number | null | undefined;
216
+ /** Localized user-message disclosure actions, including custom UserMessageBody renderers. */
217
+ userMessageDisclosureLabels?: UserMessageDisclosureLabels | undefined;
120
218
  /** Raw session events (projected internally) … */
121
219
  events?: SessionEvent[] | undefined;
122
220
  /** … or pre-projected items (e.g. from `useSessionEvents().timeline`). */
123
221
  items?: TimelineItem[] | undefined;
124
222
  /** Current session status (reserved; tip "Working…" chrome removed for now). */
125
223
  status?: SessionStatus | null | undefined;
224
+ /** Host-owned controls beside Copy and the timestamp on settled message rows. */
225
+ renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
226
+ /**
227
+ * Who sent a person's message, drawn above its bubble. Called only for
228
+ * messages that carry a sender; return null for the viewer's own messages
229
+ * or when the host has no identity to show. Omit to show no senders.
230
+ */
231
+ renderMessageSender?: RenderMessageSender | undefined;
126
232
  /** Plug a markdown renderer for message bodies (e.g. streamdown). */
127
233
  renderMessageText?:
128
- | ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
234
+ | ((
235
+ text: string,
236
+ item: AgentMessageItem | UserMessageItem,
237
+ context: { searchTarget: TimelineSearchTarget | null },
238
+ ) => ReactNode)
129
239
  | undefined;
240
+ /**
241
+ * Render assistant `opengeni-html` / `opengeni-site` fences in the default
242
+ * message renderer (`SessionConversation` supplies the inline Site preview).
243
+ */
244
+ renderInteractiveBlock?: MarkdownProps["renderInteractiveBlock"];
130
245
  /** Drill into a spawned worker session. */
131
246
  onOpenSession?: ((sessionId: string) => void) | undefined;
247
+ /**
248
+ * Name the agents this session spawns, messages, and hears from. Return the
249
+ * current display title for a session id, or null when unknown; the
250
+ * timeline falls back to the title given at spawn, then to generic copy.
251
+ * Keep the function identity stable across renders.
252
+ */
253
+ resolveSessionTitle?: ResolveSessionTitle | undefined;
132
254
  /**
133
255
  * Deep-link a memory row (a `memory.saved` / `memory.corrected` step) to its
134
256
  * record in the host's memory pane. Opt-in, exactly like `onReconnect`: the
135
257
  * library draws no "View in memory" affordance without a handler — the memory
136
258
  * row is then non-interactive rich content. This is the switch that makes the
137
- * deep-link a first-party OpenGeni capability without other SDK consumers
259
+ * deep-link a first-party Opengeni capability without other SDK consumers
138
260
  * opting into it. The app supplies it (it owns routing to the memory pane).
139
261
  */
140
262
  onMemoryClick?: ((memoryId: string) => void) | undefined;
@@ -146,6 +268,17 @@ export type MessageTimelineProps = {
146
268
  * a Reconnect button without a handler to run it.
147
269
  */
148
270
  onReconnect?: ((item: AuthNeededItem) => void | Promise<void>) | undefined;
271
+ /** Host-owned inline connection setup. Return undefined to use the default recovery card. */
272
+ renderAuthNeeded?: ((item: AuthNeededItem) => ReactNode | undefined) | undefined;
273
+ /**
274
+ * Replace the "usage limit reached" row shown when a workspace or member
275
+ * usage allowance stops work (an `allowance_exhausted` refusal). Receives the
276
+ * typed refusal (scope, reset time) and the default row; return undefined to
277
+ * keep the default. Use it to point people at your own plan or admin page.
278
+ */
279
+ renderAllowanceExhausted?: RenderAllowanceExhausted | undefined;
280
+ /** Replace the words of the default "usage limit reached" row. */
281
+ allowanceExhaustedLabels?: Partial<AllowanceLabels> | undefined;
149
282
  /**
150
283
  * Decide which durable authentication notices this timeline presents.
151
284
  * Defaults to showing every notice. Embedded hosts can suppress notices for
@@ -166,6 +299,16 @@ export type MessageTimelineProps = {
166
299
  * `createDefaultToolRegistry({ entries })` to add custom tool renderers.
167
300
  */
168
301
  toolRegistry?: ToolRegistry | undefined;
302
+ /**
303
+ * Open Opengeni object links the agent writes in replies and progress notes:
304
+ * `artifact:<file>`, `sandbox:<path>`, editable artifacts, and Sites. Return
305
+ * a host URL (`{ href }`) or action (`{ open }`); unhandled targets render as
306
+ * unavailable text instead of a console link that 404s inside the host.
307
+ * Applies to the default message renderer and to any `Markdown` a custom
308
+ * `renderMessageText` renders. `SessionConversation` supplies file and
309
+ * sandbox downloads by default.
310
+ */
311
+ resolveLink?: OpenGeniLinkResolver | undefined;
169
312
  /** Resolve opaque retained screenshot receipts through the authenticated host SDK. */
170
313
  loadRetainedScreenshot?: RetainedScreenshotLoader | undefined;
171
314
  /** Resolve permanent workspace image/file receipts through the authenticated host SDK. */
@@ -178,11 +321,22 @@ export type MessageTimelineProps = {
178
321
  */
179
322
  computeLabel?: string | null | undefined;
180
323
  /** Customize collapsed turn facets for this timeline instance. */
324
+ genieLoading?: GenieLoadingOptions | undefined;
181
325
  turnSummary?: TurnSummaryOptions | undefined;
182
326
  /** Follow new events when pinned to the bottom. Defaults to true. */
183
327
  autoFollow?: boolean | undefined;
184
328
  /** Capture a same-row text selection into the host's canonical composer draft. */
185
329
  onAnnotate?: ((annotation: DraftTimelineAnnotation) => void) | undefined;
330
+ /**
331
+ * Hide the floating "Back to your message" pill while the timeline viewport
332
+ * is shorter than this (px), e.g. a narrow embed above a decision card.
333
+ * Defaults to 0 (always available).
334
+ */
335
+ questionNavMinViewportHeight?: number | undefined;
336
+ /** Composer draft quotes currently attached to the next send. */
337
+ draftAnnotations?: readonly DraftTimelineAnnotation[] | undefined;
338
+ /** Open the composer review list for one numbered draft badge. */
339
+ onDraftAnnotationSelect?: ((id: string) => void) | undefined;
186
340
  /** Older durable history exists above the current window (see useSessionEvents). */
187
341
  hasOlder?: boolean | undefined;
188
342
  /** An older window is being fetched; shows the quiet top shimmer. */
@@ -201,13 +355,16 @@ export type MessageTimelineProps = {
201
355
  hasNewer?: boolean | undefined;
202
356
  /** A newer history page is being fetched. */
203
357
  loadingNewer?: boolean | undefined;
204
- /** Page forward through history without loading the whole gap to the tip. */
205
- onLoadNewer?: (() => void) | undefined;
358
+ /** Page forward through history. Return the request promise to enable inline error/retry. */
359
+ onLoadNewer?: (() => unknown) | undefined;
206
360
  /**
207
361
  * Reload the live tip window. When omitted, Jump to latest only re-pins and
208
362
  * scrolls the in-memory window.
209
363
  */
210
364
  onJumpToLatest?: (() => void | Promise<void>) | undefined;
365
+ /** @deprecated Retained for source compatibility; contextual prompt navigation
366
+ * uses only mounted prompts and never invokes this global resolver. */
367
+ onJumpToLatestQuestion?: (() => Promise<number | null>) | undefined;
211
368
  /** Host-owned content appended after timeline groups, such as startup progress. */
212
369
  trailingState?: ReactNode | undefined;
213
370
  emptyState?: ReactNode | undefined;
@@ -246,6 +403,24 @@ const PIN_THRESHOLD_PX = 48;
246
403
  * line-sized streaming movement.
247
404
  */
248
405
  const JUMP_TO_LATEST_CATCHUP_DEBT_PX = 240;
406
+ /**
407
+ * Floating timeline navigation: two identical small round arrow buttons at fixed
408
+ * spots, centered on the conversation. Back to your message floats 12px below
409
+ * the top edge, or just below an expanded work-header strip while that strip is
410
+ * pinned (32px, 44px on coarse pointers) so the strip stays a full-width
411
+ * target. Jump to latest floats 12px above the bottom edge. They never dodge
412
+ * the content beneath them (floating over a sliver of text is fine), so they
413
+ * cannot wander as rows stream in or the host resizes. Coarse pointers get a
414
+ * larger invisible hit area instead of a larger button.
415
+ */
416
+ const NAV_BUTTON_CLASS =
417
+ "pointer-events-auto relative inline-flex size-8 items-center justify-center rounded-full border border-og-border bg-og-surface-3/90 text-og-fg-muted shadow-og-md backdrop-blur hover:border-og-border-strong hover:text-og-fg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-og-accent pointer-coarse:before:absolute pointer-coarse:before:-inset-1.5 pointer-coarse:before:content-['']";
418
+ const NAV_FADE = {
419
+ initial: { opacity: 0 },
420
+ animate: { opacity: 1 },
421
+ exit: { opacity: 0 },
422
+ transition: { duration: 0.15, ease: "easeOut" },
423
+ } as const;
249
424
  /**
250
425
  * Prefetch older history when the top sentinel is this far from the viewport.
251
426
  * After a page loads we stay cool until the reader leaves this band (scrolls
@@ -254,13 +429,21 @@ const JUMP_TO_LATEST_CATCHUP_DEBT_PX = 240;
254
429
  const OLDER_PREFETCH_MARGIN_PX = 400;
255
430
  const OLDER_PREFETCH_ROOT_MARGIN = `${OLDER_PREFETCH_MARGIN_PX}px 0px 0px 0px`;
256
431
  const PRIMARY_ACTION_CLASS =
257
- "inline-flex w-full shrink-0 items-center justify-center gap-1.5 rounded-og-md bg-og-accent px-3 py-1.5 text-og-menu font-medium text-og-accent-fg sm:w-auto";
432
+ "inline-flex w-full shrink-0 items-center justify-center gap-1.5 rounded-og-md border border-og-primary-border bg-og-primary text-og-primary-fg px-3 py-1.5 text-og-menu font-medium sm:w-auto";
258
433
  const MESSAGE_BUBBLE_CLASS =
259
434
  "w-fit max-w-full min-w-0 rounded-og-lg rounded-br-og-xs border border-og-border bg-og-surface-2 px-4 py-2.5 text-og-md leading-6 text-og-fg";
260
435
  const WAITING_PILL_CLASS =
261
436
  "border-og-status-waiting/35 bg-og-status-waiting/10 text-og-status-waiting";
262
437
  const LOADING_CHIP_CLASS =
263
438
  "pointer-events-none inline-flex items-center rounded-full border border-og-border bg-og-surface-3/90 px-3 py-1 text-og-control font-medium shadow-og-md backdrop-blur";
439
+ /** How long live progress notes take to fold into (or out of) their work row. */
440
+ const LIVE_NOTE_FOLD_MS = 220;
441
+ /** The timeline list's `gap-5`; a folded note gives it back with `-mt-5`. */
442
+ const LIVE_NOTE_GAP_PX = 20;
443
+ /** Longest a bottom-space loan waits for an opening work list to grow into it. */
444
+ const LIVE_NOTE_RESERVE_MS = 700;
445
+ /** Lets a live work disclosure report whether it is open (see `reportLiveFold`). */
446
+ const LiveNoteFoldContext = createContext<((workId: string, open: boolean) => void) | null>(null);
264
447
 
265
448
  // State: 0 underfill retry, 1 prefetch pending, 2 compatibility-settled,
266
449
  // 3 explicit no-progress waiting to become an underfill retry.
@@ -278,13 +461,18 @@ function invokeOlderLoad(
278
461
  load: OlderHistoryLoader,
279
462
  noProgress: () => void,
280
463
  attempt: OlderLoadAttempt,
464
+ preserveTail = false,
281
465
  ): 1 | undefined {
282
466
  try {
283
467
  // Receipt creation is captured synchronously through legacy wrappers such
284
468
  // as `() => void loadOlder()`, even when the wrapper discards the return.
285
- const result = invokeOlderHistoryLoaderWithReceiptCapture(load, (receipt) => {
286
- attempt[2] = receipt;
287
- }) as OlderHistoryLoadReceipt | PromiseLike<unknown> | unknown;
469
+ const result = invokeOlderHistoryLoaderWithReceiptCapture(
470
+ load,
471
+ (receipt) => {
472
+ attempt[2] = receipt;
473
+ },
474
+ preserveTail,
475
+ ) as OlderHistoryLoadReceipt | PromiseLike<unknown> | unknown;
288
476
  const receipt =
289
477
  attempt[2] ??
290
478
  (typeof (result as { committed?: unknown } | undefined)?.committed === "boolean"
@@ -364,6 +552,49 @@ function isNearBottom(node: HTMLElement): boolean {
364
552
  return gap < Math.min(PIN_THRESHOLD_PX, maxScroll);
365
553
  }
366
554
 
555
+ /** Content-space top of a timeline group, or null when it is not mounted. */
556
+ function contentTopOf(node: HTMLElement, groupKey: string): number | null {
557
+ const element = node.querySelector(`[data-og-group-key="${cssEscapeAttribute(groupKey)}"]`);
558
+ if (!(element instanceof HTMLElement)) {
559
+ return null;
560
+ }
561
+ return element.getBoundingClientRect().top - node.getBoundingClientRect().top + node.scrollTop;
562
+ }
563
+
564
+ /** The question the reader is inside of, once its start has scrolled out of view. */
565
+ /**
566
+ * `belowHeader`: an expanded work header is pinned to the top of the scrollport,
567
+ * so the up arrow sits just below that strip instead of covering it.
568
+ */
569
+ type QuestionNav = { key: string; belowHeader: boolean };
570
+
571
+ function readQuestionNav(node: HTMLElement): QuestionNav | null {
572
+ const view = node.getBoundingClientRect();
573
+ const prompts = [...node.querySelectorAll<HTMLElement>("[data-og-prompt]")].flatMap((prompt) => {
574
+ const key = prompt.dataset.ogGroupKey;
575
+ if (!key) return [];
576
+ const bounds = prompt.getBoundingClientRect();
577
+ return [{ key, top: bounds.top, bottom: bounds.bottom }];
578
+ });
579
+ const key = questionNavTarget(prompts, { top: view.top, height: view.height });
580
+ return key ? { key, belowHeader: workHeaderPinned(node, view) } : null;
581
+ }
582
+
583
+ /** True while an expanded outer work header is stuck to the scrollport's top edge. */
584
+ function workHeaderPinned(node: HTMLElement, view: DOMRect): boolean {
585
+ for (const header of node.querySelectorAll<HTMLElement>(
586
+ '[data-og-work-header="outer"][data-state="open"]',
587
+ )) {
588
+ const box = header.getBoundingClientRect();
589
+ if (Math.abs(box.top - view.top) <= 1 && box.bottom > view.top) return true;
590
+ }
591
+ return false;
592
+ }
593
+
594
+ function sameQuestionNav(a: QuestionNav | null, b: QuestionNav | null): boolean {
595
+ return a === b || (!!a && !!b && a.key === b.key && a.belowHeader === b.belowHeader);
596
+ }
597
+
367
598
  /** Escape a value for use inside a CSS attribute selector. */
368
599
  function cssEscapeAttribute(value: string): string {
369
600
  if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
@@ -379,13 +610,24 @@ function cssEscapeAttribute(value: string): string {
379
610
  * with a "jump to latest" affordance when the reader scrolls back.
380
611
  */
381
612
  export function MessageTimeline({
613
+ resolveLink,
614
+ renderInteractiveBlock,
615
+ userMessageDisclosureLabels,
616
+ searchTarget,
617
+ focusSequence,
382
618
  events,
383
619
  items,
384
620
  status: _status,
621
+ renderMessageActions,
622
+ renderMessageSender,
385
623
  renderMessageText,
386
624
  onOpenSession,
625
+ resolveSessionTitle,
387
626
  onMemoryClick,
388
627
  onReconnect,
628
+ renderAuthNeeded,
629
+ renderAllowanceExhausted,
630
+ allowanceExhaustedLabels,
389
631
  shouldRenderAuthNeeded,
390
632
  resolveProviderLogo,
391
633
  toolRegistry = defaultToolRegistry,
@@ -393,9 +635,13 @@ export function MessageTimeline({
393
635
  loadRetainedArtifact,
394
636
  loadVideoArtifactPlayback,
395
637
  computeLabel = null,
638
+ genieLoading,
396
639
  turnSummary,
397
640
  autoFollow = true,
398
641
  onAnnotate,
642
+ questionNavMinViewportHeight = 0,
643
+ draftAnnotations,
644
+ onDraftAnnotationSelect,
399
645
  hasOlder = false,
400
646
  loadingOlder = false,
401
647
  onLoadOlder,
@@ -409,6 +655,7 @@ export function MessageTimeline({
409
655
  emptyState,
410
656
  className,
411
657
  }: MessageTimelineProps) {
658
+ const formatError = useErrorMessage();
412
659
  const resolvedItems = useMemo(() => {
413
660
  const projectedItems = items ?? buildTimeline(events ?? []);
414
661
  if (!shouldRenderAuthNeeded) {
@@ -418,9 +665,134 @@ export function MessageTimeline({
418
665
  (item) => item.kind !== "auth-needed" || shouldRenderAuthNeeded(item),
419
666
  );
420
667
  }, [items, events, shouldRenderAuthNeeded]);
421
- const sourceItems = items || events;
668
+ const searchItem = searchTarget
669
+ ? resolvedItems.find((item) => isTimelineSearchTarget(item, searchTarget))
670
+ : undefined;
671
+ const searchRevealKey =
672
+ searchItem && searchTarget
673
+ ? JSON.stringify([
674
+ searchItem.id,
675
+ searchTarget.query,
676
+ searchTarget.occurrence ?? 0,
677
+ searchTarget.offset ?? null,
678
+ ])
679
+ : null;
680
+ const searchTargetRef = useRef(searchTarget);
681
+ searchTargetRef.current = searchTarget;
682
+ const renderSearchableMessageText = useMemo(
683
+ () =>
684
+ renderMessageText
685
+ ? (text: string, item: AgentMessageItem | UserMessageItem) =>
686
+ renderMessageText(text, item, {
687
+ searchTarget: searchItem?.id === item.id ? (searchTarget ?? null) : null,
688
+ })
689
+ : (text: string, item: AgentMessageItem | UserMessageItem) => {
690
+ const body = (
691
+ <Markdown
692
+ searchTarget={searchItem?.id === item.id ? searchTarget : null}
693
+ softLineBreaks
694
+ streaming={item.kind === "agent-message" && item.streaming}
695
+ renderInteractiveBlock={
696
+ item.kind === "agent-message" ? renderInteractiveBlock : undefined
697
+ }
698
+ >
699
+ {text}
700
+ </Markdown>
701
+ );
702
+ return item.kind === "user-message" ? (
703
+ <UserMessageBody messageId={item.id} text={text}>
704
+ {body}
705
+ </UserMessageBody>
706
+ ) : (
707
+ body
708
+ );
709
+ },
710
+ [renderInteractiveBlock, renderMessageText, searchItem?.id, searchTarget],
711
+ );
712
+ // Event-window identity is independent of projected rows (partial messages
713
+ // can acquire a different first-delta id when older text arrives).
714
+ const sourceItems = events ?? items;
422
715
  const olderBoundaryKey = sourceItems?.[0]?.id;
423
- const allGroups = useMemo(() => groupTimeline(resolvedItems), [resolvedItems]);
716
+ const newerBoundaryKey = `${events?.[0]?.sessionId ?? ""}:${olderBoundaryKey ?? ""}`;
717
+ const newerScopeRef = useRef(newerBoundaryKey);
718
+ newerScopeRef.current = newerBoundaryKey;
719
+ const newerAttemptRef = useRef<{ pending: boolean } | null>(null);
720
+ const newerRetryButtonRef = useRef<HTMLButtonElement | null>(null);
721
+ const [newerRetryPending, setNewerRetryPending] = useState(false);
722
+ const [newerFailure, setNewerFailure] = useState<{ key: string; message: string } | null>(null);
723
+ useEffect(() => {
724
+ newerScopeRef.current = newerBoundaryKey;
725
+ newerAttemptRef.current = null;
726
+ setNewerFailure(null);
727
+ setNewerRetryPending(false);
728
+ return () => {
729
+ newerScopeRef.current = "";
730
+ newerAttemptRef.current = null;
731
+ };
732
+ }, [newerBoundaryKey]);
733
+ const requestNewer = useCallback(
734
+ (explicitRetry = false) => {
735
+ if (
736
+ newerScopeRef.current !== newerBoundaryKey ||
737
+ !onLoadNewer ||
738
+ loadingNewer ||
739
+ (newerAttemptRef.current && (!explicitRetry || newerAttemptRef.current.pending))
740
+ ) {
741
+ return;
742
+ }
743
+ const attempt = { pending: true };
744
+ newerAttemptRef.current = attempt;
745
+ if (explicitRetry) setNewerRetryPending(true);
746
+ const isCurrent = () =>
747
+ newerAttemptRef.current === attempt && newerScopeRef.current === newerBoundaryKey;
748
+ // Both synchronous host errors and rejected promises belong to this
749
+ // boundary. Retain the failed attempt so observers cannot hot-retry it.
750
+ void Promise.resolve()
751
+ .then(() => (isCurrent() ? onLoadNewer() : undefined))
752
+ .then(
753
+ () => {
754
+ if (!isCurrent()) return;
755
+ newerAttemptRef.current = null;
756
+ // The successful page removes the recovery control. Return focus
757
+ // to the reading surface without moving the reader's viewport.
758
+ if (document.activeElement === newerRetryButtonRef.current) {
759
+ scrollRef.current?.focus({ preventScroll: true });
760
+ }
761
+ setNewerFailure(null);
762
+ setNewerRetryPending(false);
763
+ },
764
+ (reason: unknown) => {
765
+ if (!isCurrent()) return;
766
+ attempt.pending = false;
767
+ setNewerRetryPending(false);
768
+ setNewerFailure({
769
+ key: newerBoundaryKey,
770
+ message: formatError(reason),
771
+ });
772
+ },
773
+ );
774
+ },
775
+ [onLoadNewer, loadingNewer, newerBoundaryKey, formatError],
776
+ );
777
+ const previousSourceIdsRef = useRef(new Set<string>());
778
+ const previousSourceBoundaryRef = useRef<string | undefined>(undefined);
779
+ const readingAnchorRef = useRef<TimelineAnchor | null>(null);
780
+ const settlementRef = useRef<TimelineSettlement | null>(null);
781
+ const settlementAnimationRef = useRef<Animation | null>(null);
782
+ const stopSettlement = useCallback(() => {
783
+ settlementAnimationRef.current?.cancel();
784
+ settlementAnimationRef.current = null;
785
+ }, []);
786
+ const olderPageBudgetRef = useRef(0);
787
+ const [olderDemand, setOlderDemand] = useState(0);
788
+ const readableTurns = turnSummary?.rolling === true;
789
+ const projectedGroups = useMemo(
790
+ () => groupTimeline(resolvedItems, { readableTurns }),
791
+ [resolvedItems, readableTurns],
792
+ );
793
+ // A new timeline revision (including session/turn replacement) owns layout.
794
+ // Never leave an old decorative transform running on its retained rows.
795
+ useLayoutEffect(() => stopSettlement, [projectedGroups, stopSettlement]);
424
796
  const annotationSources = useMemo(() => {
425
797
  const sources = new Map<string, TimelineAnnotationSourceDescriptor>();
426
798
  for (const item of resolvedItems) {
@@ -440,6 +812,11 @@ export function MessageTimeline({
440
812
  const bottomSentinelRef = useRef<HTMLDivElement | null>(null);
441
813
  const previousBulkFirstKeyRef = useRef<string | null | undefined>(undefined);
442
814
  const [pinned, setPinned] = useState(true);
815
+ const { groups: allGroups, release: releaseProgress } = useReadingProgress(
816
+ projectedGroups,
817
+ autoFollow && pinned && !hasNewer,
818
+ scrollRef.current,
819
+ );
443
820
  const [canSkipTipCatchup, setCanSkipTipCatchup] = useState(false);
444
821
  const canSkipTipCatchupRef = useRef(false);
445
822
  const [bulkActive, setBulkActive] = useState(true);
@@ -461,6 +838,17 @@ export function MessageTimeline({
461
838
  );
462
839
  const underfillRetryReadyRef = useRef(false);
463
840
  const resizeFollowRafRef = useRef<number | null>(null);
841
+ const questionNavFrameRef = useRef<number | null>(null);
842
+ const [questionNav, setQuestionNav] = useState<QuestionNav | null>(null);
843
+ const questionNavMinViewportRef = useRef(questionNavMinViewportHeight);
844
+ questionNavMinViewportRef.current = questionNavMinViewportHeight;
845
+ const scheduleQuestionNavRef = useRef<() => void>(() => {});
846
+ // Unpinned is not the same as away from the tip: focusing a control in the
847
+ // conversation (Copy, a connection card) hands the view to the reader while
848
+ // they are still at the bottom. Offer the jump only once there is something
849
+ // below them, or a newer window / catch-up to skip.
850
+ const [awayFromTip, setAwayFromTip] = useState(false);
851
+ const jumpPillShown = autoFollow && ((!pinned && awayFromTip) || hasNewer || canSkipTipCatchup);
464
852
  const firstGroupKey = allGroups[0] ? timelineGroupKey(allGroups[0]) : null;
465
853
  // Content stays invisible until the tip is hard-parked across a short
466
854
  // post-commit settle (two rAFs). That absorbs sync late layout while hidden
@@ -507,7 +895,7 @@ export function MessageTimeline({
507
895
  * jumps (Vimium) settle via scrollend while the camera is idle.
508
896
  */
509
897
  const readerIntentArmRef = useRef(false);
510
- /** Gesture-start geometry; cumulative tiny pointer scrolls share one budget. */
898
+ /** Gesture-start geometry survives commits before the native scroll event. */
511
899
  const readerIntentStartRef = useRef<{ scrollTop: number; maxScroll: number } | null>(null);
512
900
  /**
513
901
  * Count of camera/snap scrollTop writes whose scroll echoes are not yet
@@ -533,6 +921,191 @@ export function MessageTimeline({
533
921
  // deliberate chip remounts (activity→turn wrap, nested key flips) so a fold
534
922
  // that already settled closed — or that the reader closed — never reopens.
535
923
  const foldMemoryRef = useRef<Map<string, FoldRestingState>>(new Map());
924
+ // Live work disclosures that are open. While open, that turn's progress
925
+ // notes read inside the disclosure, so their copies above it fold away.
926
+ const [openLiveFolds, setOpenLiveFolds] = useState<ReadonlySet<string>>(() => new Set());
927
+ const openLiveFoldsRef = useRef<ReadonlySet<string>>(openLiveFolds);
928
+ const liveNoteIdsByWorkRef = useRef<ReadonlyMap<string, readonly string[]>>(new Map());
929
+ const liveFoldMotionRef = useRef<{ frame: number | null; finish: () => void } | null>(null);
930
+ // The bottom-space loan currently on the scroller, by owner.
931
+ const liveFoldReserveRef = useRef<object | null>(null);
932
+ // writeScrollTop is declared further down; the fold reaches it through this.
933
+ const writeScrollTopRef = useRef<(node: HTMLElement, top: number) => void>(() => {});
934
+ const reportLiveFold = useCallback((workId: string, open: boolean) => {
935
+ const current = openLiveFoldsRef.current;
936
+ if (current.has(workId) === open) return;
937
+ const next = new Set(current);
938
+ if (open) next.add(workId);
939
+ else next.delete(workId);
940
+ openLiveFoldsRef.current = next;
941
+ liveFoldMotionRef.current?.finish();
942
+ const scroller = scrollRef.current;
943
+ const rows = (liveNoteIdsByWorkRef.current.get(workId) ?? []).flatMap((id) => {
944
+ const row = scroller?.querySelector<HTMLElement>(
945
+ `[data-og-live-note-id="${cssEscapeAttribute(id)}"]`,
946
+ );
947
+ return row ? [row] : [];
948
+ });
949
+ if (!scroller || rows.length === 0) {
950
+ setOpenLiveFolds(next);
951
+ return;
952
+ }
953
+ const header = scroller.querySelector<HTMLElement>(
954
+ `[data-og-work-id="${cssEscapeAttribute(workId)}"] [data-og-work-header="outer"]`,
955
+ );
956
+ const viewTop = scroller.getBoundingClientRect().top;
957
+ const headerTop = header?.getBoundingClientRect().top;
958
+ if (
959
+ !open &&
960
+ header &&
961
+ headerTop !== undefined &&
962
+ (headerTop < viewTop || headerTop > viewTop + scroller.clientHeight)
963
+ ) {
964
+ // Closed from the row's sticky header after reading down the list: the
965
+ // row is no longer where the reader is. Like any big collapse, land on
966
+ // it in one step — the notes return above and the closed row rests at
967
+ // the bottom edge — rather than animating content the reader can't see.
968
+ setOpenLiveFolds(next);
969
+ requestFrame(() =>
970
+ requestFrame(() => {
971
+ const row = header.closest<HTMLElement>("[data-og-timeline-group-anchor]");
972
+ if (!row?.isConnected) return;
973
+ const box = row.getBoundingClientRect();
974
+ const top = scroller.getBoundingClientRect().top;
975
+ const bottom = top + scroller.clientHeight;
976
+ // + the scroller's pb-6, as at the true end.
977
+ if (box.bottom > bottom || box.top < top)
978
+ writeScrollTopRef.current(scroller, scroller.scrollTop + box.bottom - bottom + 24);
979
+ scheduleQuestionNavRef.current();
980
+ }),
981
+ );
982
+ return;
983
+ }
984
+ // Opening a live row near the bottom drops its one-line preview while its
985
+ // list grows from nothing; closing shrinks the list before the notes have
986
+ // grown back. For that moment the page is shorter than where the reader
987
+ // is, so the browser would clamp the scroll and everything would drop.
988
+ // This runs before that frame paints: keep the reader's position and lend
989
+ // one screen of space at the bottom (the shortfall keeps changing through
990
+ // the commit) until the content has grown back into it.
991
+ const readerTop = lastScrollTopRef.current;
992
+ const reserve = readerTop > 0.5 ? scroller.clientHeight : 0;
993
+ const loan = {};
994
+ if (reserve > 0) {
995
+ liveFoldReserveRef.current = loan;
996
+ scroller.style.paddingBottom = `calc(1.5rem + ${reserve}px)`;
997
+ if (Math.abs(scroller.scrollTop - readerTop) > 0.5)
998
+ writeScrollTopRef.current(scroller, readerTop);
999
+ }
1000
+ const releaseReserve = (deadline: number) => {
1001
+ // Only this motion's loan; a newer toggle may have replaced it.
1002
+ if (reserve === 0 || liveFoldReserveRef.current !== loan) return;
1003
+ const needed = scroller.scrollTop + scroller.clientHeight - (scroller.scrollHeight - reserve);
1004
+ if (needed > 0.5 && performance.now() < deadline && scroller.isConnected) {
1005
+ requestFrame(() => releaseReserve(deadline));
1006
+ return;
1007
+ }
1008
+ liveFoldReserveRef.current = null;
1009
+ scroller.style.removeProperty("padding-bottom");
1010
+ };
1011
+ // The notes fold into (or out of) the work row in one short motion. The
1012
+ // row glides to where it should rest: opening, into the first note's place
1013
+ // (or the top edge, when that note was above view) so its list starts just
1014
+ // beneath; closing, down below its returning notes but never past the
1015
+ // bottom edge. When everything is in view that is exactly what happens on
1016
+ // its own; otherwise the scroll is written in the same frame as the
1017
+ // heights, so it cannot lag. Native scroll anchoring is suspended for the
1018
+ // motion; inline styles pin the current look across the state commit, end
1019
+ // exactly at the classes' resting geometry, and are then removed.
1020
+ const workRow = header?.closest<HTMLElement>("[data-og-timeline-group-anchor]") ?? null;
1021
+ const rowStart = workRow?.getBoundingClientRect().top;
1022
+ const viewBottom = viewTop + scroller.clientHeight;
1023
+ let landing =
1024
+ open && rowStart !== undefined
1025
+ ? Math.max(rows[0]!.getBoundingClientRect().top, viewTop)
1026
+ : null;
1027
+ const heights = rows.map((row) => (open ? row.offsetHeight : 0));
1028
+ const apply = (progress: number) => {
1029
+ const shown = open ? 1 - progress : progress;
1030
+ rows.forEach((note, index) => {
1031
+ // The list gap travels inside the row as padding (margin cancels it),
1032
+ // so a shrinking row never overlaps the one above it mid-motion.
1033
+ note.style.marginTop = `${-LIVE_NOTE_GAP_PX}px`;
1034
+ note.style.paddingTop = `${LIVE_NOTE_GAP_PX * shown}px`;
1035
+ note.style.height = `${(heights[index]! + LIVE_NOTE_GAP_PX) * shown}px`;
1036
+ note.style.opacity = String(shown);
1037
+ });
1038
+ if (workRow && rowStart !== undefined && landing !== null) {
1039
+ const drift =
1040
+ workRow.getBoundingClientRect().top - (rowStart + (landing - rowStart) * progress);
1041
+ if (Math.abs(drift) > 0.5) writeScrollTopRef.current(scroller, scroller.scrollTop + drift);
1042
+ }
1043
+ };
1044
+ for (const row of rows) {
1045
+ row.style.boxSizing = "border-box";
1046
+ row.style.overflow = "hidden";
1047
+ row.style.overflowAnchor = "none";
1048
+ }
1049
+ scroller.style.overflowAnchor = "none";
1050
+ apply(0);
1051
+ const fold: { frame: number | null; finish: () => void } = {
1052
+ frame: null,
1053
+ finish: () => {
1054
+ if (fold.frame != null) cancelFrame(fold.frame);
1055
+ for (const row of rows) {
1056
+ for (const property of [
1057
+ "height",
1058
+ "margin-top",
1059
+ "padding-top",
1060
+ "opacity",
1061
+ "overflow",
1062
+ "overflow-anchor",
1063
+ "box-sizing",
1064
+ ])
1065
+ row.style.removeProperty(property);
1066
+ }
1067
+ scroller.style.removeProperty("overflow-anchor");
1068
+ releaseReserve(performance.now() + LIVE_NOTE_RESERVE_MS);
1069
+ // The row may now be pinned to the top without any scroll: re-place
1070
+ // the up arrow (it sits below a pinned work header).
1071
+ scheduleQuestionNavRef.current();
1072
+ if (liveFoldMotionRef.current === fold) liveFoldMotionRef.current = null;
1073
+ },
1074
+ };
1075
+ liveFoldMotionRef.current = fold;
1076
+ const duration = prefersReducedMotion() ? 0 : LIVE_NOTE_FOLD_MS;
1077
+ let began: number | null = null;
1078
+ const step = (now: number) => {
1079
+ fold.frame = null;
1080
+ if (!rows.every((row) => row.isConnected)) return fold.finish();
1081
+ if (!open && began === null) {
1082
+ // Unfolding: the natural heights are known only once the folded
1083
+ // class is gone; the inline styles still hold them at zero.
1084
+ rows.forEach((note, index) => (heights[index] = note.scrollHeight));
1085
+ if (workRow && rowStart !== undefined) {
1086
+ const returning = heights.reduce((sum, height) => sum + height + LIVE_NOTE_GAP_PX, 0);
1087
+ // The closed row is its header plus one preview line (its list is
1088
+ // still collapsing now, so the row's own height is not final yet).
1089
+ const closedRow = 2 * (header?.getBoundingClientRect().height ?? 0);
1090
+ const lowest = viewBottom - closedRow - 24;
1091
+ landing = Math.min(rowStart + returning, Math.max(lowest, rowStart));
1092
+ }
1093
+ }
1094
+ began ??= now;
1095
+ const linear = duration === 0 ? 1 : Math.min(1, (now - began) / duration);
1096
+ apply(1 - (1 - linear) ** 3);
1097
+ if (linear < 1) fold.frame = requestFrame(step);
1098
+ else fold.finish();
1099
+ };
1100
+ fold.frame = requestFrame(step);
1101
+ setOpenLiveFolds(next);
1102
+ }, []);
1103
+ useEffect(
1104
+ () => () => {
1105
+ liveFoldMotionRef.current?.finish();
1106
+ },
1107
+ [],
1108
+ );
536
1109
  const userMessageDisclosureMemoryRef = useRef<Map<string, boolean>>(new Map());
537
1110
  const seenActivityIdsRef = useRef<Set<string>>(new Set());
538
1111
  const firstItemGroupKeyRef = useRef<string | null>(null);
@@ -557,6 +1130,34 @@ export function MessageTimeline({
557
1130
  previousBulkFirstKeyRef.current !== firstGroupKey;
558
1131
  const bulkRender = allGroups.length > 0 && (bulkActive || firstKeyChangedForBulk);
559
1132
  const groups = useStableTimelineGroupKeys(allGroups, !bulkRender);
1133
+ // Each live progress note → whether its turn's work disclosure is open.
1134
+ const [liveNoteFolds, liveNoteIdsByWork] = useMemo(() => {
1135
+ const folds = new Map<string, boolean>();
1136
+ const byWork = new Map<string, readonly string[]>();
1137
+ for (const { group } of groups) {
1138
+ if (group.kind !== "activity" || !group.work?.liveNoteIds) continue;
1139
+ byWork.set(group.id, group.work.liveNoteIds);
1140
+ for (const id of group.work.liveNoteIds) folds.set(id, openLiveFolds.has(group.id));
1141
+ }
1142
+ return [folds, byWork] as const;
1143
+ }, [groups, openLiveFolds]);
1144
+ useLayoutEffect(() => {
1145
+ liveNoteIdsByWorkRef.current = liveNoteIdsByWork;
1146
+ }, [liveNoteIdsByWork]);
1147
+ const turnsWithOutput = useMemo(
1148
+ () =>
1149
+ new Set(
1150
+ resolvedItems.flatMap((item) =>
1151
+ "turnId" in item &&
1152
+ item.turnId &&
1153
+ (item.kind === "tool-call" ||
1154
+ ((item.kind === "agent-message" || item.kind === "reasoning") && item.text.trim()))
1155
+ ? [item.turnId]
1156
+ : [],
1157
+ ),
1158
+ ),
1159
+ [resolvedItems],
1160
+ );
560
1161
 
561
1162
  const applyCanSkipTipCatchup = useCallback((value: boolean) => {
562
1163
  if (canSkipTipCatchupRef.current !== value) {
@@ -611,6 +1212,9 @@ export function MessageTimeline({
611
1212
  if (before === next) {
612
1213
  return;
613
1214
  }
1215
+ // Unpinned camera/anchor writes invalidate return intent. A pinned
1216
+ // pointer gesture still needs its cumulative leave budget across writes.
1217
+ if (!pinnedRef.current || !readerIntentArmRef.current) readerIntentStartRef.current = null;
614
1218
  programmaticScrollRef.current += 1;
615
1219
  node.scrollTop = next;
616
1220
  if (node.scrollTop === before) {
@@ -620,6 +1224,7 @@ export function MessageTimeline({
620
1224
  programmaticScrollRef.current -= 1;
621
1225
  }
622
1226
  }, []);
1227
+ writeScrollTopRef.current = writeScrollTop;
623
1228
 
624
1229
  const cancelLeaveFallback = useCallback(() => {
625
1230
  if (leaveFallbackRafRef.current != null) {
@@ -648,29 +1253,43 @@ export function MessageTimeline({
648
1253
  }, []);
649
1254
 
650
1255
  /** Reader left the tip — wheel, keyboard, pointer-armed scroll-up, or scrollend. */
651
- const releasePinFromReader = useCallback(
652
- (node?: HTMLElement | null) => {
653
- if (!autoFollow || !pinnedRef.current || hasNewerRef.current) {
654
- return;
655
- }
656
- // Unscrollable window: unpin strands Jump-to-latest with no way back.
657
- if (node && maxScrollOf(node) <= 1) {
658
- return;
659
- }
660
- clearReaderIntent();
661
- clearPendingReaderLeave();
662
- stopFollow();
663
- applyPinned(false);
664
- if (wantPinRef.current) {
665
- wantPinRef.current = false;
666
- }
667
- if (!olderPrefetchArmedRef.current) {
668
- olderPrefetchArmedRef.current = true;
669
- setOlderPrefetchArmed(true);
1256
+ const releasePinFromReader = useCallback(() => {
1257
+ if (!autoFollow || !pinnedRef.current || hasNewerRef.current) {
1258
+ return;
1259
+ }
1260
+ clearReaderIntent();
1261
+ clearPendingReaderLeave();
1262
+ stopFollow();
1263
+ applyPinned(false);
1264
+ if (wantPinRef.current) {
1265
+ wantPinRef.current = false;
1266
+ }
1267
+ if (!olderPrefetchArmedRef.current) {
1268
+ olderPrefetchArmedRef.current = true;
1269
+ setOlderPrefetchArmed(true);
1270
+ }
1271
+ }, [autoFollow, applyPinned, clearPendingReaderLeave, clearReaderIntent, stopFollow]);
1272
+
1273
+ useEffect(() => {
1274
+ if (!readableTurns) return;
1275
+ const node = scrollRef.current;
1276
+ if (!node) return;
1277
+ const onSelection = () => {
1278
+ const selection = node.ownerDocument.getSelection();
1279
+ if (
1280
+ selection &&
1281
+ !selection.isCollapsed &&
1282
+ node.contains(selection.anchorNode) &&
1283
+ node.contains(selection.focusNode)
1284
+ ) {
1285
+ stopSettlement();
1286
+ releasePinFromReader();
1287
+ disclosureKeepsUnpinnedRef.current = true;
670
1288
  }
671
- },
672
- [autoFollow, applyPinned, clearPendingReaderLeave, clearReaderIntent, stopFollow],
673
- );
1289
+ };
1290
+ node.ownerDocument.addEventListener("selectionchange", onSelection);
1291
+ return () => node.ownerDocument.removeEventListener("selectionchange", onSelection);
1292
+ }, [readableTurns, releasePinFromReader, stopSettlement]);
674
1293
 
675
1294
  /**
676
1295
  * Settled away from the tip while the camera is idle — Vimium / unfocused
@@ -691,7 +1310,7 @@ export function MessageTimeline({
691
1310
  clearPendingReaderLeave();
692
1311
  return;
693
1312
  }
694
- releasePinFromReader(node);
1313
+ releasePinFromReader();
695
1314
  rearmOlderPrefetchAfterLeavingTop(node);
696
1315
  },
697
1316
  [autoFollow, clearPendingReaderLeave, rearmOlderPrefetchAfterLeavingTop, releasePinFromReader],
@@ -712,6 +1331,22 @@ export function MessageTimeline({
712
1331
  });
713
1332
  }, [cancelLeaveFallback, releasePinAfterScrollSettled]);
714
1333
 
1334
+ const requestEarlierFromReader = () => {
1335
+ // Upward navigation replaces any earlier downward gesture, even when
1336
+ // already unpinned and the engine never delivered scrollend.
1337
+ clearReaderIntent();
1338
+ releasePinFromReader();
1339
+ wantPinRef.current = false;
1340
+ // A stationary upward gesture is still demand. Successful short/folded
1341
+ // pages may never create enough range to leave the prefetch band.
1342
+ olderPageBudgetRef.current = 8;
1343
+ if (olderLoadAttemptRef.current?.[1] === 2) {
1344
+ olderLoadAttemptRef.current = null;
1345
+ setOlderDemand((value) => value + 1);
1346
+ }
1347
+ };
1348
+ const touchPositionRef = useRef<{ x: number; y: number } | null>(null);
1349
+
715
1350
  const onWheel = (event: {
716
1351
  deltaY: number;
717
1352
  deltaX: number;
@@ -728,13 +1363,17 @@ export function MessageTimeline({
728
1363
  if (wheelConsumedByNestedScrollable(event)) {
729
1364
  return;
730
1365
  }
1366
+ stopSettlement();
731
1367
  disclosureKeepsUnpinnedRef.current = false;
1368
+ programmaticScrollRef.current = 0;
732
1369
  if (event.deltaY >= 0) {
1370
+ const node = scrollRef.current;
1371
+ readerIntentStartRef.current = node
1372
+ ? { scrollTop: node.scrollTop, maxScroll: maxScrollOf(node) }
1373
+ : null;
733
1374
  return;
734
1375
  }
735
- const node =
736
- event.currentTarget instanceof HTMLElement ? event.currentTarget : scrollRef.current;
737
- releasePinFromReader(node);
1376
+ requestEarlierFromReader();
738
1377
  };
739
1378
 
740
1379
  /** Touch / stylus / mouse drag on the scroller — explicit leave (not layout). */
@@ -748,6 +1387,7 @@ export function MessageTimeline({
748
1387
  if (event.button && event.pointerType === "mouse") {
749
1388
  return;
750
1389
  }
1390
+ stopSettlement();
751
1391
  // Clicks on chips/buttons/links must not arm — their settle collapse
752
1392
  // also drops scrollTop and would false-unpin. Drag on prose/scroller may.
753
1393
  if (
@@ -757,6 +1397,7 @@ export function MessageTimeline({
757
1397
  return;
758
1398
  }
759
1399
  disclosureKeepsUnpinnedRef.current = false;
1400
+ programmaticScrollRef.current = 0;
760
1401
  const node =
761
1402
  event.currentTarget instanceof HTMLElement ? event.currentTarget : scrollRef.current;
762
1403
  readerIntentArmRef.current = true;
@@ -774,14 +1415,20 @@ export function MessageTimeline({
774
1415
  event.key === "Home" ||
775
1416
  event.key === "End"
776
1417
  ) {
1418
+ stopSettlement();
777
1419
  disclosureKeepsUnpinnedRef.current = false;
1420
+ // Reader navigation supersedes a pending camera/anchor scroll echo,
1421
+ // including downward keys that do not explicitly release the pin.
1422
+ programmaticScrollRef.current = 0;
1423
+ const node = scrollRef.current;
1424
+ readerIntentStartRef.current = node
1425
+ ? { scrollTop: node.scrollTop, maxScroll: maxScrollOf(node) }
1426
+ : null;
778
1427
  }
779
1428
  if (event.key !== "ArrowUp" && event.key !== "PageUp" && event.key !== "Home") {
780
1429
  return;
781
1430
  }
782
- const node =
783
- event.currentTarget instanceof HTMLElement ? event.currentTarget : scrollRef.current;
784
- releasePinFromReader(node);
1431
+ requestEarlierFromReader();
785
1432
  };
786
1433
 
787
1434
  const snapToBottom = useCallback(
@@ -864,22 +1511,33 @@ export function MessageTimeline({
864
1511
  () => ({
865
1512
  expandedByMessageId: userMessageDisclosureMemoryRef.current,
866
1513
  beginChange: beginUserMessageDisclosureChange,
1514
+ labels: {
1515
+ showMore: userMessageDisclosureLabels?.showMore,
1516
+ showLess: userMessageDisclosureLabels?.showLess,
1517
+ },
867
1518
  }),
868
- [beginUserMessageDisclosureChange],
1519
+ [
1520
+ beginUserMessageDisclosureChange,
1521
+ userMessageDisclosureLabels?.showMore,
1522
+ userMessageDisclosureLabels?.showLess,
1523
+ ],
869
1524
  );
870
1525
  const timelineGroupEntryContext = useMemo<TimelineGroupEntryContext>(
871
1526
  () => ({
872
1527
  userMessageDisclosureContext,
873
1528
  behavior: {
874
- renderMessageText,
1529
+ renderMessageActions,
1530
+ renderMessageText: renderSearchableMessageText,
875
1531
  onOpenSession,
876
1532
  onMemoryClick,
877
1533
  onReconnect,
1534
+ renderAuthNeeded,
878
1535
  resolveProviderLogo,
879
1536
  toolRegistry,
880
1537
  loadRetainedScreenshot,
881
1538
  loadRetainedArtifact,
882
1539
  loadVideoArtifactPlayback,
1540
+ genieLoading,
883
1541
  turnSummary,
884
1542
  },
885
1543
  }),
@@ -890,14 +1548,30 @@ export function MessageTimeline({
890
1548
  onMemoryClick,
891
1549
  onOpenSession,
892
1550
  onReconnect,
893
- renderMessageText,
1551
+ renderAuthNeeded,
1552
+ renderMessageActions,
1553
+ renderSearchableMessageText,
894
1554
  resolveProviderLogo,
895
1555
  toolRegistry,
1556
+ genieLoading,
896
1557
  turnSummary,
897
1558
  userMessageDisclosureContext,
898
1559
  ],
899
1560
  );
900
1561
 
1562
+ const releaseSearchPin = useCallback(() => {
1563
+ applyPinned(false);
1564
+ stopFollow();
1565
+ setRevealed(true);
1566
+ }, [applyPinned, stopFollow]);
1567
+ useTimelineSearchNavigation(
1568
+ scrollRef,
1569
+ searchItem?.id,
1570
+ searchTarget,
1571
+ releaseSearchPin,
1572
+ resolvedItems,
1573
+ );
1574
+
901
1575
  const requestOlderIfUnderfilled = useCallback(
902
1576
  (node: HTMLElement, retry?: OlderLoadAttempt) => {
903
1577
  let currentAttempt = olderLoadAttemptRef.current;
@@ -947,10 +1621,78 @@ export function MessageTimeline({
947
1621
  // exact `false` is the first-party request-not-accepted receipt.
948
1622
  // All other fulfillment retains this exact owner until its prepend
949
1623
  // boundary commits; promise settlement alone cannot prove progress.
950
- invokeOlderLoad(onLoadOlder, noProgress, attempt);
1624
+ invokeOlderLoad(onLoadOlder, noProgress, attempt, !retry);
951
1625
  },
952
1626
  [hasOlder, loadingOlder, olderBoundaryKey, onLoadOlder],
953
1627
  );
1628
+ const scheduleQuestionNav = useCallback(() => {
1629
+ if (!readableTurns || questionNavFrameRef.current != null) {
1630
+ return;
1631
+ }
1632
+ questionNavFrameRef.current = requestFrame(() => {
1633
+ questionNavFrameRef.current = null;
1634
+ const node = scrollRef.current;
1635
+ // A short viewport (a narrow embed above a decision card) has no room
1636
+ // for a floating pill that would cover the very rows being read.
1637
+ // clientHeight <= 1 is pre-layout/headless, not a short viewport.
1638
+ const roomy =
1639
+ node !== null &&
1640
+ (node.clientHeight <= 1 || node.clientHeight >= questionNavMinViewportRef.current);
1641
+ const next = node && roomy ? readQuestionNav(node) : null;
1642
+ setQuestionNav((current) => (sameQuestionNav(current, next) ? current : next));
1643
+ });
1644
+ }, [readableTurns]);
1645
+ scheduleQuestionNavRef.current = scheduleQuestionNav;
1646
+ useEffect(
1647
+ () => () => {
1648
+ if (questionNavFrameRef.current != null) {
1649
+ cancelFrame(questionNavFrameRef.current);
1650
+ questionNavFrameRef.current = null;
1651
+ }
1652
+ },
1653
+ [],
1654
+ );
1655
+ const jumpToQuestion = (key: string) => {
1656
+ const node = scrollRef.current;
1657
+ const top = node ? contentTopOf(node, key) : null;
1658
+ if (!node || top === null) {
1659
+ return;
1660
+ }
1661
+ // Explicit reader navigation: leave the tip and park the question on top.
1662
+ releasePinFromReader();
1663
+ wantPinRef.current = false;
1664
+ disclosureKeepsUnpinnedRef.current = false;
1665
+ writeScrollTop(node, Math.max(0, top - QUESTION_NAV_MARGIN_PX));
1666
+ // Hand keyboard/accessibility focus to the actual destination. This also
1667
+ // gives native scroll anchoring the new question rather than an old work
1668
+ // row when the bounded context subsequently gains a later history page.
1669
+ node
1670
+ .querySelector<HTMLElement>(`[data-og-group-key="${cssEscapeAttribute(key)}"]`)
1671
+ ?.focus({ preventScroll: true });
1672
+ syncScrollBaseline(node);
1673
+ scheduleQuestionNav();
1674
+ };
1675
+
1676
+ // Land once on the moment an inbox item or a notification points at.
1677
+ // Deliberately dependency-free: it retries after every commit until the
1678
+ // target row is laid out, and the ref guard makes the state update run once
1679
+ // per focus sequence, so it cannot chain renders.
1680
+ const focusedSequenceRef = useRef<number | null>(null);
1681
+ // oxlint-disable-next-line react-hooks/exhaustive-deps -- retries each commit; ref-guarded to run once
1682
+ useLayoutEffect(() => {
1683
+ if (!focusSequence || focusedSequenceRef.current === focusSequence) return;
1684
+ const index = timelineGroupIndexAtSequence(
1685
+ groups.map((entry) => entry.group),
1686
+ focusSequence,
1687
+ );
1688
+ const key = index >= 0 ? groups[index]?.key : undefined;
1689
+ const node = scrollRef.current;
1690
+ if (!key || !node || contentTopOf(node, key) === null) return;
1691
+ focusedSequenceRef.current = focusSequence;
1692
+ setRevealed(true);
1693
+ jumpToQuestion(key);
1694
+ });
1695
+
954
1696
  const driveFollowRef = useRef<(node: HTMLElement, now?: number) => void>(
955
1697
  requestOlderIfUnderfilled as (node: HTMLElement, now?: number) => void,
956
1698
  );
@@ -1103,15 +1845,28 @@ export function MessageTimeline({
1103
1845
  previousMaxScroll <= 1 ||
1104
1846
  previousMaxScroll - previousScrollTop < Math.min(PIN_THRESHOLD_PX, previousMaxScroll);
1105
1847
  const firstItemChanged = !!previousFirstItemId && firstItemId !== previousFirstItemId;
1106
- const prepended =
1107
- firstItemChanged && resolvedItems.some((item) => item.id === previousFirstItemId);
1848
+ const sourceChanged = olderBoundaryKey !== previousSourceBoundaryRef.current;
1849
+ const retainedSource =
1850
+ sourceItems?.some((item) => previousSourceIdsRef.current.has(item.id)) ?? false;
1851
+ const prepended = sourceChanged && retainedSource;
1852
+ previousSourceBoundaryRef.current = olderBoundaryKey;
1853
+ previousSourceIdsRef.current = new Set(sourceItems?.map((item) => item.id));
1854
+ const readingAnchor = readingAnchorRef.current;
1855
+ readingAnchorRef.current = null;
1856
+ const settlement = settlementRef.current;
1857
+ settlementRef.current = null;
1108
1858
  const attempt = olderLoadAttemptRef.current;
1109
1859
  const committedZeroOverlapOlderReplacement = !!(
1110
1860
  attempt?.[2]?.committed &&
1111
- firstItemChanged &&
1112
- !prepended
1861
+ sourceChanged &&
1862
+ !retainedSource
1113
1863
  );
1114
1864
  const restorePrependAnchor = () => {
1865
+ const correction = readingAnchor && timelineAnchorCorrection(node, readingAnchor);
1866
+ if (correction != null) {
1867
+ if (Math.abs(correction) > 1) writeScrollTop(node, node.scrollTop + correction);
1868
+ return;
1869
+ }
1115
1870
  // Keep the reader on the same retained rows. Prefer the exact first-item
1116
1871
  // content coordinate (needed when a prepend merges inside one group),
1117
1872
  // then the retained group offset, then scrollHeight as a final fallback.
@@ -1183,17 +1938,23 @@ export function MessageTimeline({
1183
1938
  applyPinned(false);
1184
1939
  }
1185
1940
  } else if (prepended) {
1941
+ const olderAttemptState = olderLoadAttemptRef.current?.[1];
1942
+ const underfillOwned =
1943
+ !!olderLoadAttemptRef.current &&
1944
+ olderAttemptState !== 1 &&
1945
+ olderAttemptState !== 2 &&
1946
+ olderAttemptState !== 3;
1186
1947
  if (
1187
1948
  autoFollow &&
1188
1949
  pinnedRef.current &&
1189
1950
  !hasNewer &&
1190
1951
  !pendingReaderLeaveRef.current &&
1191
- (wasAtLiveTailBeforeCommit || olderLoadAttemptRef.current)
1952
+ (wasAtLiveTailBeforeCommit || underfillOwned)
1192
1953
  ) {
1193
- // A pending short-window load owns the prepend even when live growth
1194
- // has left its still-pinned camera with raw geometry debt. Unrelated
1195
- // prepends retain the prior-tip fallback so a stale pin after an
1196
- // extension/programmatic history jump still restores its row anchor.
1954
+ // Still following the live tip: underfill, or a prefetch the reader
1955
+ // started and then returned from. Park at the new tip. A stale pin
1956
+ // while they are actually up in a short window (gap is not inside
1957
+ // PIN_THRESHOLD of maxScroll, so wasAtLiveTail is false) restores.
1197
1958
  clearPendingReaderLeave();
1198
1959
  snapToBottom(node);
1199
1960
  } else {
@@ -1206,6 +1967,22 @@ export function MessageTimeline({
1206
1967
  applyPinned(false);
1207
1968
  }
1208
1969
  }
1970
+ } else if (!pinnedRef.current && readingAnchor && readableTurns) {
1971
+ // A live work row moves behind newly streamed prose. Correct only the
1972
+ // residual movement native anchoring did not absorb, using the reader's
1973
+ // retained paragraph/control rather than the moving row's outer box.
1974
+ const correction = timelineAnchorCorrection(node, readingAnchor);
1975
+ if (correction !== null && Math.abs(correction) > 1) {
1976
+ writeScrollTop(node, node.scrollTop + correction);
1977
+ }
1978
+ const focused = readingAnchor.find((anchor) => anchor.focused)?.element;
1979
+ if (
1980
+ focused &&
1981
+ node.contains(focused) &&
1982
+ node.ownerDocument.activeElement === node.ownerDocument.body
1983
+ ) {
1984
+ focused.focus({ preventScroll: true });
1985
+ }
1209
1986
  } else if (autoFollow && pinnedRef.current && !hasNewer) {
1210
1987
  // Load/remount (still hidden): hard-park. Live tip after reveal: ease.
1211
1988
  // Pending unarmed leave: tip *growth* must not yank (Vimium during stream).
@@ -1227,6 +2004,7 @@ export function MessageTimeline({
1227
2004
  if (prepended && !pinnedRef.current && node.scrollTop > OLDER_PREFETCH_MARGIN_PX) {
1228
2005
  rearmOlderPrefetchAfterLeavingTop(node);
1229
2006
  }
2007
+ scheduleQuestionNav();
1230
2008
  previousFirstItemIdRef.current = firstItemId;
1231
2009
  previousScrollHeightRef.current = node.scrollHeight;
1232
2010
  syncScrollBaseline(node);
@@ -1257,6 +2035,8 @@ export function MessageTimeline({
1257
2035
  : null;
1258
2036
  }
1259
2037
 
2038
+ if (settlement) settlementAnimationRef.current = animateTimelineSettlement(settlement);
2039
+
1260
2040
  // Promise settlement is not itself permission to retry. A receipt-marked
1261
2041
  // accepted page retires its exact owner on this commit even when projection
1262
2042
  // is empty or merges into the same first item. Without that mark, retain
@@ -1283,6 +2063,19 @@ export function MessageTimeline({
1283
2063
  // Boundary progress retires the request owner. A late settlement from
1284
2064
  // that completed prefetch cannot mutate the new window's cooldown owner.
1285
2065
  olderLoadAttemptRef.current = [olderBoundaryKey, 2];
2066
+ // Bound automatic work, but let continued upward input replenish demand.
2067
+ // Only a committed first-party receipt proves that another page is safe.
2068
+ if (
2069
+ attempt[2]?.committed &&
2070
+ !pinnedRef.current &&
2071
+ hasOlder &&
2072
+ node.scrollTop <= OLDER_PREFETCH_MARGIN_PX &&
2073
+ olderPageBudgetRef.current > 0
2074
+ ) {
2075
+ olderPageBudgetRef.current -= 1;
2076
+ olderLoadAttemptRef.current = null;
2077
+ setOlderDemand((value) => value + 1);
2078
+ }
1286
2079
  rearmOlderPrefetchAfterLeavingTop(node);
1287
2080
  requestOlderIfUnderfilled(node);
1288
2081
  });
@@ -1453,6 +2246,7 @@ export function MessageTimeline({
1453
2246
  loadingOlder,
1454
2247
  olderBoundaryKey,
1455
2248
  olderPrefetchArmed,
2249
+ olderDemand,
1456
2250
  onLoadOlder,
1457
2251
  requestOlderIfUnderfilled,
1458
2252
  ]);
@@ -1474,15 +2268,17 @@ export function MessageTimeline({
1474
2268
  }
1475
2269
  const observer = new IntersectionObserver(
1476
2270
  (entries) => {
1477
- if (entries.some((entry) => entry.isIntersecting)) {
1478
- onLoadNewer();
2271
+ // An underfilled history window has both sentinels visible. Advancing
2272
+ // it automatically would undo an explicit older-page navigation.
2273
+ if (maxScrollOf(root) > 1 && entries.some((entry) => entry.isIntersecting)) {
2274
+ requestNewer();
1479
2275
  }
1480
2276
  },
1481
2277
  { root, rootMargin: "0px 0px 1200px 0px" },
1482
2278
  );
1483
2279
  observer.observe(target);
1484
2280
  return () => observer.disconnect();
1485
- }, [hasNewer, loadingNewer, onLoadNewer, firstGroupKey]);
2281
+ }, [hasNewer, loadingNewer, onLoadNewer, requestNewer, firstGroupKey]);
1486
2282
 
1487
2283
  // Late layout that React commits cannot see (images decoding, fonts, code
1488
2284
  // blocks) grows content without a commit. While pinned, soft-follow the tip;
@@ -1504,6 +2300,9 @@ export function MessageTimeline({
1504
2300
  if (!current) {
1505
2301
  return;
1506
2302
  }
2303
+ // A viewport that shrank below the pill's minimum hides it now, not
2304
+ // on the next scroll.
2305
+ if (questionNavMinViewportRef.current > 0) scheduleQuestionNavRef.current();
1507
2306
  requestOlderIfUnderfilled(current);
1508
2307
  if (!autoFollow || !pinnedRef.current || hasNewerRef.current) {
1509
2308
  return;
@@ -1535,6 +2334,8 @@ export function MessageTimeline({
1535
2334
  // at what just became the live bottom — paging forward to the tip must not
1536
2335
  // strand them unpinned watching new content grow below.
1537
2336
  useEffect(() => {
2337
+ // Closing find is paint-only; it must not trigger this history-to-tip rule.
2338
+ if (searchTargetRef.current) return;
1538
2339
  if (hasNewer) {
1539
2340
  stopFollow();
1540
2341
  applyPinned(false);
@@ -1557,6 +2358,15 @@ export function MessageTimeline({
1557
2358
  }
1558
2359
  }, [hasNewer, autoFollow, applyPinned, snapToBottom, stopFollow]);
1559
2360
 
2361
+ // After the scroll authority above has settled each commit, record whether
2362
+ // the reader is away from the tip (content can land below an unpinned
2363
+ // reader without any scroll event).
2364
+ // oxlint-disable-next-line react-hooks/exhaustive-deps -- Deliberately runs after every commit.
2365
+ useLayoutEffect(() => {
2366
+ const node = scrollRef.current;
2367
+ if (node && !pinnedRef.current) setAwayFromTip(!isNearBottom(node));
2368
+ });
2369
+
1560
2370
  // Pinned: layout/camera recover tip debt; wheel/keys/pointer-arm unpin
1561
2371
  // immediately; extension jumps settle via scrollend (or one-rAF fallback).
1562
2372
  // Do not tip-follow-yank an in-flight unarmed scroll-away — that ate Vimium.
@@ -1565,6 +2375,9 @@ export function MessageTimeline({
1565
2375
  if (!node) {
1566
2376
  return;
1567
2377
  }
2378
+ // Only an unpinned reader's distance matters for the jump.
2379
+ if (!pinnedRef.current) setAwayFromTip(!isNearBottom(node));
2380
+ scheduleQuestionNav();
1568
2381
  const previousTop = lastScrollTopRef.current;
1569
2382
  const previousMaxScroll = lastMaxScrollRef.current;
1570
2383
  const nextTop = node.scrollTop;
@@ -1598,6 +2411,19 @@ export function MessageTimeline({
1598
2411
  return;
1599
2412
  }
1600
2413
 
2414
+ if (programmatic && !pinnedRef.current) {
2415
+ // Restore/camera writes while reading history are not a return to the tip.
2416
+ // Still expire a pending Jump-to-latest latch: the in-window jump itself
2417
+ // is a programmatic snap, and a later reader scroll-away can arrive
2418
+ // before that echo is consumed. Skipping this left the latch armed and
2419
+ // snapped the reader when hasNewer later flipped false.
2420
+ syncScrollBaseline(node);
2421
+ if (wantPinRef.current && !isNearBottom(node)) {
2422
+ wantPinRef.current = false;
2423
+ }
2424
+ return;
2425
+ }
2426
+
1601
2427
  if (autoFollow && pinnedRef.current && !hasNewer) {
1602
2428
  // Fold / composer / SessionChrome: viewport shrink raises maxScroll without
1603
2429
  // growing content. Must hit tipFollow before we adopt the new clientHeight
@@ -1632,7 +2458,7 @@ export function MessageTimeline({
1632
2458
  // Pointer-dragged scroll-up away from tip. Layout churn never arms this.
1633
2459
  if (readerArmed && cumulativeReaderUp > TIP_FOLLOW_READER_UP_EPS_PX && !nearBottomPinned) {
1634
2460
  clearReaderIntent();
1635
- releasePinFromReader(node);
2461
+ requestEarlierFromReader();
1636
2462
  rearmOlderPrefetchAfterLeavingTop(node);
1637
2463
  return;
1638
2464
  }
@@ -1657,15 +2483,46 @@ export function MessageTimeline({
1657
2483
  }
1658
2484
 
1659
2485
  syncScrollBaseline(node);
2486
+ if (programmatic) {
2487
+ // Anchor restoration is never permission to resume tip-follow.
2488
+ return;
2489
+ }
1660
2490
  const nearBottom = isNearBottom(node);
1661
2491
 
1662
- // Re-pin only when the reader moved toward/at the tip — not when a fold
1663
- // clamp dragged scrollTop down onto nearBottom.
2492
+ // Re-pin only when the reader moved toward/at the tip. Prepend restore
2493
+ // and overflow-anchor raise scrollTop by roughly the same amount as
2494
+ // maxScroll; treating that as a scroll-down
2495
+ // re-pinned a compact-tail history reader (their preserved gap falls
2496
+ // inside PIN_THRESHOLD once the window is tall) and snapped them back.
2497
+ // A commit may already have adopted this scroll position into the layout
2498
+ // baseline. Explicit wheel/key/pointer intent retains its own start so
2499
+ // returning to the tip still re-pins after that interleaving.
2500
+ // Passive wheel handlers may run after compositor scrolling; retain the
2501
+ // earlier layout baseline when it still contains that reader movement.
2502
+ const returnStart =
2503
+ readerIntentStart && readerIntentStart.scrollTop <= previousTop ? readerIntentStart : null;
2504
+ const inserted = Math.max(0, nextMaxScroll - (returnStart?.maxScroll ?? previousMaxScroll));
2505
+ const towardTip = nextTop - (returnStart?.scrollTop ?? previousTop) - inserted;
2506
+ // Explicit intent may reach the exact live bottom despite intervening
2507
+ // streaming growth. Keep subtracting that growth: native anchoring alone
2508
+ // conserves the gap and must never qualify as reader movement.
2509
+ const explicitReturnToBottom = returnStart !== null && nextMaxScroll - nextTop <= 1;
1664
2510
  const nextPinned =
1665
- !hasNewer && nearBottom && nextTop >= previousTop - TIP_FOLLOW_READER_UP_EPS_PX;
2511
+ !hasNewer && nearBottom && towardTip > 0.5 && (inserted <= 1 || explicitReturnToBottom);
1666
2512
  if (!nextPinned) {
1667
2513
  stopFollow();
1668
2514
  }
2515
+ const focused = node.ownerDocument.activeElement;
2516
+ if (
2517
+ nextPinned &&
2518
+ focused instanceof HTMLElement &&
2519
+ focused.hasAttribute("data-og-prompt") &&
2520
+ node.contains(focused)
2521
+ ) {
2522
+ // Question navigation focuses its destination. Returning to the live
2523
+ // tip releases that stale reader ownership before the commit snapshot.
2524
+ node.focus({ preventScroll: true });
2525
+ }
1669
2526
  applyPinned(nextPinned);
1670
2527
  // A far-from-bottom scroll while a Jump-to-latest is pending is the reader
1671
2528
  // changing their mind: drop the latch, or a stale one (host rejected or
@@ -1676,8 +2533,15 @@ export function MessageTimeline({
1676
2533
  wantPinRef.current = false;
1677
2534
  }
1678
2535
  if (nextPinned) {
2536
+ clearReaderIntent();
1679
2537
  return;
1680
2538
  }
2539
+ if (readerIntentStart) {
2540
+ // This scroll already accounted for the gesture's movement. Retain a
2541
+ // fresh start for continuous input across the next commit, not stale
2542
+ // movement that could turn a later native clamp into a return to tip.
2543
+ readerIntentStartRef.current = { scrollTop: nextTop, maxScroll: nextMaxScroll };
2544
+ }
1681
2545
  if (!olderPrefetchArmedRef.current) {
1682
2546
  olderPrefetchArmedRef.current = true;
1683
2547
  setOlderPrefetchArmed(true);
@@ -1693,6 +2557,9 @@ export function MessageTimeline({
1693
2557
  return;
1694
2558
  }
1695
2559
  cancelLeaveFallback();
2560
+ // Engines can emit scrollend between tiny pointer steps. Keep the pinned
2561
+ // gesture's cumulative leave budget until it actually leaves the tip.
2562
+ if (!pinnedRef.current || !readerIntentArmRef.current) clearReaderIntent();
1696
2563
  if (disclosureKeepsUnpinnedRef.current) {
1697
2564
  programmaticScrollRef.current = 0;
1698
2565
  stopFollow();
@@ -1707,263 +2574,486 @@ export function MessageTimeline({
1707
2574
  }
1708
2575
  releasePinAfterScrollSettled(node);
1709
2576
  };
2577
+ // React 18 has no `onScrollEnd` prop: it warns and never calls it, while
2578
+ // `supportsScrollEndEvent()` still makes the camera wait for scrollend. A
2579
+ // native listener (re-bound each commit, so it always sees this render's
2580
+ // handler and scroller) works on every supported React version.
2581
+ useEffect(() => {
2582
+ const node = scrollRef.current;
2583
+ if (!node) return;
2584
+ node.addEventListener("scrollend", onScrollEnd);
2585
+ return () => node.removeEventListener("scrollend", onScrollEnd);
2586
+ });
1710
2587
 
1711
- return (
1712
- <LightboxProvider>
1713
- <FoldMemoryProvider value={foldMemoryRef.current}>
1714
- <SeenActivityIdsProvider value={seenActivityIdsRef.current}>
1715
- <TimelineComputeLabelProvider value={computeLabel ?? null}>
1716
- <EntranceAnimationProvider value={false}>
1717
- <TooltipProvider delayDuration={400}>
1718
- <div className={cn("og-root relative flex min-h-0 flex-col", className)}>
1719
- {onAnnotate ? (
1720
- <Suspense fallback={null}>
1721
- <TimelineAnnotationSelection
1722
- rootRef={scrollRef}
1723
- sources={annotationSources}
1724
- onAnnotate={onAnnotate}
1725
- />
1726
- </Suspense>
1727
- ) : null}
1728
- {/* Pinned: anchoring off so the tip-follow camera owns the motion.
2588
+ const timeline = (
2589
+ <AllowancePresentationProvider
2590
+ render={renderAllowanceExhausted}
2591
+ labels={allowanceExhaustedLabels}
2592
+ >
2593
+ <LightboxProvider>
2594
+ <FoldMemoryProvider value={foldMemoryRef.current}>
2595
+ <LiveNoteFoldContext.Provider value={reportLiveFold}>
2596
+ <SeenActivityIdsProvider value={seenActivityIdsRef.current}>
2597
+ <TimelineComputeLabelProvider value={computeLabel ?? null}>
2598
+ <EntranceAnimationProvider value={false}>
2599
+ <TooltipProvider delayDuration={400}>
2600
+ <TimelineAnnotationSourceRootContext.Provider value={scrollRef}>
2601
+ <div
2602
+ className={cn("og-root relative flex min-h-0 flex-col", className)}
2603
+ // Sticky insets start at the scroller's padded content edge
2604
+ // (pt-16). Subtract exactly that padding so an expanded work
2605
+ // header pins flush to the scrollport. Pinning it lower left a
2606
+ // band of scrolling rows visible above the header, which then
2607
+ // looked like it floated over the middle of the timeline.
2608
+ style={{ "--og-work-header-top": "-4rem" } as CSSProperties}
2609
+ >
2610
+ {onAnnotate ? (
2611
+ <Suspense fallback={null}>
2612
+ <TimelineAnnotationSelection
2613
+ rootRef={scrollRef}
2614
+ sources={annotationSources}
2615
+ onAnnotate={onAnnotate}
2616
+ />
2617
+ </Suspense>
2618
+ ) : null}
2619
+ {/* Pinned: anchoring off so the tip-follow camera owns the motion.
1729
2620
  Unpinned: native scroll anchoring holds the reader's place. */}
1730
- <div
1731
- ref={scrollRef}
1732
- data-og-timeline-scroller=""
1733
- data-og-bottom-follow={autoFollow && pinned && !hasNewer ? "true" : "false"}
1734
- tabIndex={-1}
1735
- onScroll={onScroll}
1736
- onScrollEnd={onScrollEnd}
1737
- onWheel={onWheel}
1738
- onPointerDown={onPointerDown}
1739
- onKeyDown={onKeyDown}
1740
- style={groups.length > 0 && !revealed ? { visibility: "hidden" } : undefined}
1741
- className={cn(
1742
- // tabIndex=-1 is programmatic only — never paint a focus ring on
1743
- // the whole scroller (click + Shift used to flash a blue outline).
1744
- "min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 py-6 sm:px-6 outline-hidden",
1745
- autoFollow && pinned && !hasNewer
1746
- ? "[overflow-anchor:none]"
1747
- : "[overflow-anchor:auto]",
1748
- )}
1749
- >
1750
- <div className="relative mx-auto flex w-full max-w-3xl flex-col gap-5">
1751
- {!groups.length
1752
- ? (emptyState ?? (
1753
- <p className="py-10 text-center text-og-menu text-og-fg-subtle">
1754
- No activity yet.
1755
- </p>
1756
- ))
1757
- : null}
1758
- {hasOlder && olderPrefetchArmed ? (
1759
- // Overlaid, not a layout row: mounting/unmounting the sentinel
1760
- // must never shift content (that shift was itself a wobble).
1761
2621
  <div
1762
- ref={topSentinelRef}
1763
- data-og-top-sentinel=""
1764
- data-og-timeline-chrome=""
1765
- aria-hidden="true"
1766
- className="pointer-events-none absolute inset-x-0 top-0 h-px"
1767
- />
1768
- ) : null}
1769
- {groups.map(({ group, key, entranceEnabled }, index) => {
1770
- return (
1771
- <TimelineGroupEntry
1772
- key={key}
1773
- groupKey={key}
1774
- group={group}
1775
- nextGroup={groups[index + 1]?.group}
1776
- entranceEnabled={entranceEnabled}
1777
- liveEntranceEnabled={
1778
- group.kind === "activity" ? !bulkRender : undefined
2622
+ ref={scrollRef}
2623
+ data-og-timeline-scroller=""
2624
+ data-og-bottom-follow={
2625
+ autoFollow && pinned && !hasNewer ? "true" : "false"
2626
+ }
2627
+ tabIndex={-1}
2628
+ onScroll={onScroll}
2629
+ onWheel={onWheel}
2630
+ onTouchStart={(event) => {
2631
+ stopSettlement();
2632
+ const touch = event.touches.length === 1 ? event.touches[0] : undefined;
2633
+ touchPositionRef.current = touch
2634
+ ? { x: touch.clientX, y: touch.clientY }
2635
+ : null;
2636
+ }}
2637
+ onTouchMove={(event) => {
2638
+ const touch = event.touches[0];
2639
+ const previous = touchPositionRef.current;
2640
+ if (!touch || !previous || event.touches.length !== 1) {
2641
+ touchPositionRef.current = null;
2642
+ return;
1779
2643
  }
1780
- context={timelineGroupEntryContext}
1781
- />
1782
- );
1783
- })}
1784
- {groups.length > 0 && trailingState ? (
1785
- <div data-og-timeline-trailing-state="">{trailingState}</div>
1786
- ) : null}
1787
- {hasNewer ? (
1788
- <div
1789
- ref={bottomSentinelRef}
1790
- data-og-bottom-sentinel=""
1791
- data-og-timeline-chrome=""
1792
- aria-hidden="true"
1793
- className="h-px w-full"
1794
- />
1795
- ) : null}
1796
- </div>
1797
- </div>
1798
- <AnimatePresence>
1799
- {loadingOlder ||
1800
- loadingOldest ||
1801
- (hasOlder && onJumpToStart && olderPrefetchArmed) ||
1802
- underfillRetryReady ? (
1803
- // Floating over the scroller (not a timeline row) so showing and
1804
- // hiding it never reflows history under the reader.
1805
- <motion.div
1806
- initial={{ opacity: 0, y: -6 }}
1807
- animate={{ opacity: 1, y: 0 }}
1808
- exit={{ opacity: 0, y: -6 }}
1809
- transition={{ duration: 0.15, ease: "easeOut" }}
1810
- data-og-loading-older=""
1811
- aria-live="polite"
1812
- className="absolute inset-x-0 top-3 z-10 flex justify-center"
1813
- >
1814
- {loadingOlder || loadingOldest ? (
1815
- <span className={LOADING_CHIP_CLASS}>
1816
- <span className="og-shimmer-text">
1817
- {loadingOldest ? "Jumping to start…" : "Loading earlier activity…"}
1818
- </span>
1819
- </span>
1820
- ) : null}
1821
- {hasOlder &&
1822
- !loadingOlder &&
1823
- !loadingOldest &&
1824
- (underfillRetryReady || onJumpToStart) ? (
1825
- <button
1826
- type="button"
1827
- data-og-retry={underfillRetryReady || undefined}
1828
- data-og-jump-to-start={!underfillRetryReady || undefined}
1829
- onClick={() => {
1830
- const node = scrollRef.current;
1831
- if (underfillRetryReady) {
1832
- if (node && underfillRetryReadyRef.current) {
1833
- // AnimatePresence retains this handler during
1834
- // exit. Current authorization plus the exact
1835
- // attempt check prevent stale dispatch.
1836
- requestOlderIfUnderfilled(node, underfillSettledAttempt!);
1837
- }
1838
- return;
2644
+ const deltaX = previous.x - touch.clientX;
2645
+ const deltaY = previous.y - touch.clientY;
2646
+ if (Math.max(Math.abs(deltaX), Math.abs(deltaY)) < 4) return;
2647
+ touchPositionRef.current = { x: touch.clientX, y: touch.clientY };
2648
+ onWheel({
2649
+ deltaX,
2650
+ deltaY,
2651
+ target: event.target,
2652
+ currentTarget: event.currentTarget,
2653
+ });
2654
+ }}
2655
+ onTouchEnd={() => {
2656
+ touchPositionRef.current = null;
2657
+ }}
2658
+ onTouchCancel={() => {
2659
+ touchPositionRef.current = null;
2660
+ }}
2661
+ onClickCapture={(event) => {
2662
+ const target =
2663
+ event.target instanceof Element
2664
+ ? event.target.closest("button[aria-expanded]")
2665
+ : null;
2666
+ if (target) {
2667
+ if (target.hasAttribute("data-og-work-header")) {
2668
+ releaseProgress(
2669
+ target
2670
+ .closest("[data-og-group-key]")
2671
+ ?.getAttribute("data-og-group-key") ?? null,
2672
+ );
1839
2673
  }
1840
- applyPinned(false);
1841
- pendingJumpToStartRef.current = true;
1842
- const seq = ++jumpToStartSeqRef.current;
1843
- void Promise.resolve(onJumpToStart!()).then(
1844
- () => {
1845
- // The commit that swaps in the oldest window consumes
1846
- // the flag against the new DOM; this write covers the
1847
- // already-committed order and the no-window-change
1848
- // case (jumping within the current window).
1849
- const scroller = scrollRef.current ?? node;
1850
- if (scroller) {
1851
- scroller.scrollTop = 0;
1852
- }
1853
- // A host may resolve without ever changing the
1854
- // window (already on the oldest page). Any swap
1855
- // commit runs its layout effect before the next
1856
- // frame, so a flag still armed by then is the
1857
- // no-change case — clear it, or a LATER prepend
1858
- // would spuriously jump the reader to the top.
1859
- requestFrame(() => {
1860
- if (jumpToStartSeqRef.current === seq) {
1861
- pendingJumpToStartRef.current = false;
1862
- }
1863
- });
1864
- },
1865
- () => {
1866
- if (jumpToStartSeqRef.current === seq) {
1867
- pendingJumpToStartRef.current = false;
1868
- }
1869
- },
1870
- );
2674
+ releasePinFromReader();
2675
+ disclosureKeepsUnpinnedRef.current = true;
2676
+ }
2677
+ }}
2678
+ onFocusCapture={(event) => {
2679
+ if (readableTurns && event.target !== event.currentTarget) {
2680
+ stopSettlement();
2681
+ releasePinFromReader();
2682
+ disclosureKeepsUnpinnedRef.current = true;
2683
+ }
2684
+ }}
2685
+ onPointerDown={onPointerDown}
2686
+ onKeyDown={onKeyDown}
2687
+ style={
2688
+ groups.length > 0 && !revealed ? { visibility: "hidden" } : undefined
2689
+ }
2690
+ className={cn(
2691
+ // tabIndex=-1 is programmatic only — never paint a focus ring on
2692
+ // the whole scroller (click + Shift used to flash a blue outline).
2693
+ "min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pt-16 pb-6 sm:px-6 outline-hidden",
2694
+ autoFollow && pinned && !hasNewer
2695
+ ? "[overflow-anchor:none]"
2696
+ : "[overflow-anchor:auto]",
2697
+ )}
2698
+ >
2699
+ <TimelineBeforeLayout
2700
+ capture={() => {
2701
+ if (readableTurns && timelineHasReader(scrollRef.current)) {
2702
+ releasePinFromReader();
2703
+ }
2704
+ readingAnchorRef.current =
2705
+ !pinnedRef.current && scrollRef.current
2706
+ ? captureTimelineAnchor(scrollRef.current)
2707
+ : null;
2708
+ settlementRef.current =
2709
+ readableTurns &&
2710
+ autoFollow &&
2711
+ pinnedRef.current &&
2712
+ revealedRef.current &&
2713
+ !hasNewer &&
2714
+ !pendingReaderLeaveRef.current &&
2715
+ !disclosureKeepsUnpinnedRef.current &&
2716
+ !prefersReducedMotion() &&
2717
+ scrollRef.current
2718
+ ? captureTimelineSettlement(scrollRef.current, groups)
2719
+ : null;
1871
2720
  }}
1872
- className="rounded-full border border-og-border px-3 py-1.5 text-og-control"
1873
2721
  >
1874
- {underfillRetryReady ? "Retry earlier activity" : "Jump to start"}
1875
- </button>
2722
+ <div className="relative mx-auto flex w-full max-w-3xl flex-col gap-5">
2723
+ <AnimatePresence>
2724
+ {loadingOlder ||
2725
+ loadingOldest ||
2726
+ (hasOlder && onJumpToStart && olderPrefetchArmed) ||
2727
+ underfillRetryReady ? (
2728
+ // Reserved top gutter: controls scroll with history and cannot
2729
+ // cover a disclosure. Visibility never changes content height.
2730
+ <motion.div
2731
+ initial={{ opacity: 0, y: -6 }}
2732
+ animate={{ opacity: 1, y: 0 }}
2733
+ exit={{ opacity: 0, y: -6 }}
2734
+ transition={{ duration: 0.15, ease: "easeOut" }}
2735
+ data-og-loading-older=""
2736
+ aria-live="polite"
2737
+ className="pointer-events-none absolute inset-x-0 -top-11 z-10 flex justify-center"
2738
+ >
2739
+ {loadingOlder || loadingOldest ? (
2740
+ <span className={LOADING_CHIP_CLASS}>
2741
+ <span className="og-shimmer-text">
2742
+ {loadingOldest
2743
+ ? "Jumping to start…"
2744
+ : "Loading earlier activity…"}
2745
+ </span>
2746
+ </span>
2747
+ ) : null}
2748
+ {hasOlder &&
2749
+ !loadingOlder &&
2750
+ !loadingOldest &&
2751
+ (underfillRetryReady || onJumpToStart) ? (
2752
+ <button
2753
+ type="button"
2754
+ data-og-retry={underfillRetryReady || undefined}
2755
+ data-og-jump-to-start={!underfillRetryReady || undefined}
2756
+ onClick={() => {
2757
+ const node = scrollRef.current;
2758
+ if (underfillRetryReady) {
2759
+ if (node && underfillRetryReadyRef.current) {
2760
+ // AnimatePresence retains this handler during
2761
+ // exit. Current authorization plus the exact
2762
+ // attempt check prevent stale dispatch.
2763
+ requestOlderIfUnderfilled(
2764
+ node,
2765
+ underfillSettledAttempt!,
2766
+ );
2767
+ }
2768
+ return;
2769
+ }
2770
+ applyPinned(false);
2771
+ pendingJumpToStartRef.current = true;
2772
+ const seq = ++jumpToStartSeqRef.current;
2773
+ void Promise.resolve(onJumpToStart!()).then(
2774
+ () => {
2775
+ // The commit that swaps in the oldest window consumes
2776
+ // the flag against the new DOM; this write covers the
2777
+ // already-committed order and the no-window-change
2778
+ // case (jumping within the current window).
2779
+ const scroller = scrollRef.current ?? node;
2780
+ if (scroller) {
2781
+ scroller.scrollTop = 0;
2782
+ }
2783
+ // A host may resolve without ever changing the
2784
+ // window (already on the oldest page). Any swap
2785
+ // commit runs its layout effect before the next
2786
+ // frame, so a flag still armed by then is the
2787
+ // no-change case — clear it, or a LATER prepend
2788
+ // would spuriously jump the reader to the top.
2789
+ requestFrame(() => {
2790
+ if (jumpToStartSeqRef.current === seq) {
2791
+ pendingJumpToStartRef.current = false;
2792
+ }
2793
+ });
2794
+ },
2795
+ () => {
2796
+ if (jumpToStartSeqRef.current === seq) {
2797
+ pendingJumpToStartRef.current = false;
2798
+ }
2799
+ },
2800
+ );
2801
+ }}
2802
+ className="pointer-events-auto rounded-full border border-og-border px-3 py-1.5 text-og-control"
2803
+ >
2804
+ {underfillRetryReady
2805
+ ? underfillSettledAttempt?.[2]?.tailPreserved
2806
+ ? "Load earlier activity"
2807
+ : "Retry earlier activity"
2808
+ : "Jump to start"}
2809
+ </button>
2810
+ ) : null}
2811
+ </motion.div>
2812
+ ) : null}
2813
+ </AnimatePresence>
2814
+ {!groups.length
2815
+ ? (emptyState ?? (
2816
+ <p className="py-10 text-center text-og-menu text-og-fg-subtle">
2817
+ No activity yet.
2818
+ </p>
2819
+ ))
2820
+ : null}
2821
+ {hasOlder && olderPrefetchArmed ? (
2822
+ // Overlaid, not a layout row: mounting/unmounting the sentinel
2823
+ // must never shift content (that shift was itself a wobble).
2824
+ // End at the scroll origin above the pt-16 gutter so the
2825
+ // observer and scrollTop cooldown share the same 400px band.
2826
+ <div
2827
+ ref={topSentinelRef}
2828
+ data-og-top-sentinel=""
2829
+ data-og-timeline-chrome=""
2830
+ aria-hidden="true"
2831
+ className="pointer-events-none absolute inset-x-0 -top-16 h-px -translate-y-full"
2832
+ />
2833
+ ) : null}
2834
+ {groups.map(({ group, key, entranceEnabled }, index) => {
2835
+ return (
2836
+ <TimelineSearchRevealContext.Provider
2837
+ key={key}
2838
+ value={
2839
+ searchItem &&
2840
+ timelineGroupItemIds(group).includes(searchItem.id)
2841
+ ? searchRevealKey
2842
+ : null
2843
+ }
2844
+ >
2845
+ <TimelineGroupEntry
2846
+ groupKey={key}
2847
+ group={group}
2848
+ nextGroup={groups[index + 1]?.group}
2849
+ startupDismissed={
2850
+ group.kind === "activity" &&
2851
+ group.items.some(
2852
+ (item) => item.turnId && turnsWithOutput.has(item.turnId),
2853
+ )
2854
+ }
2855
+ entranceEnabled={entranceEnabled}
2856
+ liveEntranceEnabled={
2857
+ group.kind === "activity" ? !bulkRender : undefined
2858
+ }
2859
+ noteFolded={
2860
+ group.kind === "item"
2861
+ ? liveNoteFolds.get(group.item.id)
2862
+ : undefined
2863
+ }
2864
+ context={timelineGroupEntryContext}
2865
+ />
2866
+ </TimelineSearchRevealContext.Provider>
2867
+ );
2868
+ })}
2869
+ {groups.length > 0 && trailingState ? (
2870
+ <div data-og-timeline-trailing-state="">{trailingState}</div>
2871
+ ) : null}
2872
+ {hasNewer && newerFailure?.key === newerBoundaryKey ? (
2873
+ <div
2874
+ data-og-newer-error=""
2875
+ className="flex flex-col items-center gap-2 px-4 py-3 text-center text-og-menu text-og-fg-muted"
2876
+ >
2877
+ <p role="status" className="max-w-prose [overflow-wrap:anywhere]">
2878
+ Couldn’t load later activity. {newerFailure.message}
2879
+ </p>
2880
+ <button
2881
+ ref={newerRetryButtonRef}
2882
+ type="button"
2883
+ data-og-retry-newer=""
2884
+ className="min-h-11 rounded-og-md border border-og-border px-3 py-2 text-og-fg hover:bg-og-surface-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-og-accent"
2885
+ aria-disabled={loadingNewer || newerRetryPending}
2886
+ aria-busy={newerRetryPending}
2887
+ onClick={() => requestNewer(true)}
2888
+ >
2889
+ {newerRetryPending
2890
+ ? "Retrying later activity…"
2891
+ : "Retry later activity"}
2892
+ </button>
2893
+ </div>
2894
+ ) : null}
2895
+ {hasNewer ? (
2896
+ <div
2897
+ ref={bottomSentinelRef}
2898
+ data-og-bottom-sentinel=""
2899
+ data-og-timeline-chrome=""
2900
+ aria-hidden="true"
2901
+ className="h-px w-full"
2902
+ />
2903
+ ) : null}
2904
+ </div>
2905
+ </TimelineBeforeLayout>
2906
+ </div>
2907
+ {draftAnnotations && draftAnnotations.length > 0 ? (
2908
+ <Suspense fallback={null}>
2909
+ <TimelineAnnotationMarkers
2910
+ rootRef={scrollRef}
2911
+ annotations={draftAnnotations}
2912
+ onSelect={onDraftAnnotationSelect}
2913
+ />
2914
+ </Suspense>
1876
2915
  ) : null}
1877
- </motion.div>
1878
- ) : null}
1879
- </AnimatePresence>
1880
- <AnimatePresence>
1881
- {loadingNewer ? (
1882
- <motion.div
1883
- initial={{ opacity: 0, y: 6 }}
1884
- animate={{ opacity: 1, y: 0 }}
1885
- exit={{ opacity: 0, y: 6 }}
1886
- transition={{ duration: 0.15, ease: "easeOut" }}
1887
- data-og-loading-newer=""
1888
- aria-live="polite"
1889
- className="pointer-events-none absolute inset-x-0 bottom-14 z-10 flex justify-center"
1890
- >
1891
- <span className={LOADING_CHIP_CLASS}>
1892
- <span className="og-shimmer-text">Loading later activity…</span>
1893
- </span>
1894
- </motion.div>
1895
- ) : null}
1896
- </AnimatePresence>
1897
- <AnimatePresence>
1898
- {((!pinned && autoFollow) || hasNewer || canSkipTipCatchup) && autoFollow ? (
1899
- <motion.button
1900
- type="button"
1901
- data-og-jump-to-latest=""
1902
- initial={{ opacity: 0, y: 8 }}
1903
- animate={{ opacity: 1, y: 0 }}
1904
- exit={{ opacity: 0, y: 8 }}
1905
- transition={{ duration: 0.15, ease: "easeOut" }}
1906
- onClick={() => {
1907
- disclosureKeepsUnpinnedRef.current = false;
1908
- if (hasNewer) {
1909
- // Do not pin against the current history page — its bottom
1910
- // is not the tip. The pin + snap run when the tip window
1911
- // actually lands (`hasNewer` flips false).
1912
- wantPinRef.current = true;
1913
- const node = scrollRef.current;
1914
- if (onJumpToLatest) {
1915
- void Promise.resolve(onJumpToLatest()).then(
1916
- () => {
1917
- // Covers a host that flipped hasNewer before
1918
- // resolving; otherwise the tip-window commit
1919
- // consumes the flag.
1920
- const current = scrollRef.current;
1921
- if (current && wantPinRef.current && !hasNewerRef.current) {
1922
- wantPinRef.current = false;
2916
+
2917
+ <div
2918
+ data-og-timeline-nav="top"
2919
+ className={cn(
2920
+ "pointer-events-none absolute inset-x-0 z-10 flex justify-center",
2921
+ questionNav?.belowHeader ? "top-10 pointer-coarse:top-13" : "top-3",
2922
+ )}
2923
+ >
2924
+ <AnimatePresence>
2925
+ {questionNav ? (
2926
+ <motion.button
2927
+ key="question-nav"
2928
+ {...NAV_FADE}
2929
+ type="button"
2930
+ data-og-question-nav=""
2931
+ data-og-jump-to-question=""
2932
+ title="Back to your message"
2933
+ onClick={() => jumpToQuestion(questionNav.key)}
2934
+ className={NAV_BUTTON_CLASS}
2935
+ >
2936
+ <ArrowUpIcon aria-hidden className="size-4" />
2937
+ <span className="sr-only">Back to your message</span>
2938
+ </motion.button>
2939
+ ) : null}
2940
+ </AnimatePresence>
2941
+ </div>
2942
+ <div
2943
+ data-og-timeline-nav="bottom"
2944
+ className="pointer-events-none absolute inset-x-0 bottom-3 z-10 flex flex-col items-center gap-2"
2945
+ >
2946
+ <AnimatePresence>
2947
+ {loadingNewer ? (
2948
+ <motion.div
2949
+ key="loading-newer"
2950
+ {...NAV_FADE}
2951
+ data-og-loading-newer=""
2952
+ aria-live="polite"
2953
+ className="flex max-w-full justify-center"
2954
+ >
2955
+ <span className={LOADING_CHIP_CLASS}>
2956
+ <span className="og-shimmer-text">Loading later activity…</span>
2957
+ </span>
2958
+ </motion.div>
2959
+ ) : null}
2960
+ {jumpPillShown ? (
2961
+ <motion.button
2962
+ key="jump-to-latest"
2963
+ {...NAV_FADE}
2964
+ type="button"
2965
+ data-og-jump-to-latest=""
2966
+ title="Jump to latest"
2967
+ onClick={() => {
2968
+ // Returning to the tip explicitly releases reader-owned
2969
+ // prose. Clear only this timeline's selection before the
2970
+ // synchronous ownership check on the next commit.
2971
+ const viewport = scrollRef.current;
2972
+ const selection = viewport?.ownerDocument.getSelection();
2973
+ if (
2974
+ selection &&
2975
+ !selection.isCollapsed &&
2976
+ (viewport?.contains(selection.anchorNode) ||
2977
+ viewport?.contains(selection.focusNode))
2978
+ )
2979
+ selection.removeAllRanges();
2980
+ disclosureKeepsUnpinnedRef.current = false;
2981
+ if (hasNewer) {
2982
+ // Do not pin against the current history page — its bottom
2983
+ // is not the tip. The pin + snap run when the tip window
2984
+ // actually lands (`hasNewer` flips false).
2985
+ wantPinRef.current = true;
2986
+ const node = scrollRef.current;
2987
+ if (onJumpToLatest) {
2988
+ void Promise.resolve(onJumpToLatest()).then(
2989
+ () => {
2990
+ // Covers a host that flipped hasNewer before
2991
+ // resolving; otherwise the tip-window commit
2992
+ // consumes the flag.
2993
+ const current = scrollRef.current;
2994
+ if (
2995
+ current &&
2996
+ wantPinRef.current &&
2997
+ !hasNewerRef.current
2998
+ ) {
2999
+ wantPinRef.current = false;
3000
+ applyPinned(true);
3001
+ snapToBottom(current);
3002
+ }
3003
+ },
3004
+ () => {
3005
+ // The tip reload failed (ordinary network error):
3006
+ // an armed latch would fire a surprise snap when
3007
+ // the reader later pages to the tip themselves.
3008
+ wantPinRef.current = false;
3009
+ },
3010
+ );
3011
+ } else if (node) {
3012
+ // No tip reload available: jump within the in-memory
3013
+ // window so the newer sentinel can page forward; the
3014
+ // latch pins if the tip window eventually lands.
3015
+ snapToBottom(node);
3016
+ }
3017
+ return;
3018
+ }
3019
+ const node = scrollRef.current;
3020
+ if (node) {
1923
3021
  applyPinned(true);
1924
- snapToBottom(current);
3022
+ snapToBottom(node);
1925
3023
  }
1926
- },
1927
- () => {
1928
- // The tip reload failed (ordinary network error):
1929
- // an armed latch would fire a surprise snap when
1930
- // the reader later pages to the tip themselves.
1931
- wantPinRef.current = false;
1932
- },
1933
- );
1934
- } else if (node) {
1935
- // No tip reload available: jump within the in-memory
1936
- // window so the newer sentinel can page forward; the
1937
- // latch pins if the tip window eventually lands.
1938
- snapToBottom(node);
1939
- }
1940
- return;
1941
- }
1942
- const node = scrollRef.current;
1943
- if (node) {
1944
- applyPinned(true);
1945
- snapToBottom(node);
1946
- }
1947
- }}
1948
- className={cn(
1949
- "absolute inset-x-0 bottom-4 mx-auto w-fit",
1950
- "inline-flex items-center gap-1.5 rounded-full border border-og-border bg-og-surface-3/90 px-3 py-1.5",
1951
- "text-og-control font-medium text-og-fg shadow-og-md backdrop-blur",
1952
- "hover:border-og-border-strong",
1953
- )}
1954
- >
1955
- <ArrowDownIcon className="size-3.5" />
1956
- Jump to latest
1957
- </motion.button>
1958
- ) : null}
1959
- </AnimatePresence>
1960
- </div>
1961
- </TooltipProvider>
1962
- </EntranceAnimationProvider>
1963
- </TimelineComputeLabelProvider>
1964
- </SeenActivityIdsProvider>
1965
- </FoldMemoryProvider>
1966
- </LightboxProvider>
3024
+ }}
3025
+ className={NAV_BUTTON_CLASS}
3026
+ >
3027
+ <ArrowDownIcon aria-hidden className="size-4" />
3028
+ <span className="sr-only">Jump to latest</span>
3029
+ </motion.button>
3030
+ ) : null}
3031
+ </AnimatePresence>
3032
+ </div>
3033
+ </div>
3034
+ </TimelineAnnotationSourceRootContext.Provider>
3035
+ </TooltipProvider>
3036
+ </EntranceAnimationProvider>
3037
+ </TimelineComputeLabelProvider>
3038
+ </SeenActivityIdsProvider>
3039
+ </LiveNoteFoldContext.Provider>
3040
+ </FoldMemoryProvider>
3041
+ </LightboxProvider>
3042
+ </AllowancePresentationProvider>
3043
+ );
3044
+ // Agent-authored object links resolve through the host, never the console.
3045
+ return (
3046
+ <OpenGeniLinkProvider resolveLink={resolveLink}>
3047
+ <MessageSenderContext.Provider value={renderMessageSender}>
3048
+ <AgentIdentityProvider
3049
+ items={resolvedItems}
3050
+ resolveSessionTitle={resolveSessionTitle}
3051
+ onOpenSession={onOpenSession}
3052
+ >
3053
+ <ProviderLogoProvider value={resolveProviderLogo}>{timeline}</ProviderLogoProvider>
3054
+ </AgentIdentityProvider>
3055
+ </MessageSenderContext.Provider>
3056
+ </OpenGeniLinkProvider>
1967
3057
  );
1968
3058
  }
1969
3059
 
@@ -2001,7 +3091,11 @@ function useStableTimelineGroupKeys(
2001
3091
  const previousByItemId = new Map<string, KeyedTimelineGroup>();
2002
3092
  for (const previous of previousRef.current) {
2003
3093
  for (const itemId of timelineGroupItemIds(previous.group)) {
2004
- previousByItemId.set(itemId, previous);
3094
+ // A live progress note is both its own row and listed in its work
3095
+ // history. Its own row owns the id, so the note keeps its identity.
3096
+ if (previous.group.kind === "item" || !previousByItemId.has(itemId)) {
3097
+ previousByItemId.set(itemId, previous);
3098
+ }
2005
3099
  }
2006
3100
  }
2007
3101
 
@@ -2014,7 +3108,24 @@ function useStableTimelineGroupKeys(
2014
3108
  // Retain only same-kind matches. Activity → turn wrap must NOT keep the
2015
3109
  // activity chip's React key: that reused a collapsed TurnSummary and
2016
3110
  // skipped the settle beat (insta-collapse / content flash).
2017
- if (previous && previous.group.kind === group.kind && !usedKeys.has(previous.key)) {
3111
+ const startupCompletion =
3112
+ previous?.group.kind === "activity" &&
3113
+ previous.group.items.every(
3114
+ (item) =>
3115
+ item.kind === "startup-phase" || (item.kind === "reasoning" && !item.text.trim()),
3116
+ ) &&
3117
+ group.kind === "turn" &&
3118
+ group.outcome === "complete" &&
3119
+ group.groups.every(
3120
+ (child) =>
3121
+ child.kind === "activity" &&
3122
+ child.items.every((item) => item.kind === "startup-phase"),
3123
+ );
3124
+ if (
3125
+ previous &&
3126
+ (previous.group.kind === group.kind || startupCompletion) &&
3127
+ !usedKeys.has(previous.key)
3128
+ ) {
2018
3129
  retainedGroup = previous;
2019
3130
  break;
2020
3131
  }
@@ -2134,8 +3245,14 @@ type TimelineGroupEntryProps = {
2134
3245
  groupKey: string;
2135
3246
  group: TimelineGroup;
2136
3247
  nextGroup?: TimelineGroup | undefined;
3248
+ startupDismissed: boolean;
2137
3249
  entranceEnabled: boolean;
2138
3250
  liveEntranceEnabled?: boolean | undefined;
3251
+ /**
3252
+ * Present for a live progress note: true while its turn's work disclosure is
3253
+ * open (the note then reads inside it), false while it reads here.
3254
+ */
3255
+ noteFolded?: boolean | undefined;
2139
3256
  context: TimelineGroupEntryContext;
2140
3257
  };
2141
3258
 
@@ -2145,15 +3262,20 @@ type TimelineGroupEntryContext = {
2145
3262
  };
2146
3263
 
2147
3264
  type TimelineGroupBehaviorProps = {
2148
- renderMessageText: MessageTimelineProps["renderMessageText"];
3265
+ renderMessageActions: MessageTimelineProps["renderMessageActions"];
3266
+ renderMessageText:
3267
+ | ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
3268
+ | undefined;
2149
3269
  onOpenSession: MessageTimelineProps["onOpenSession"];
2150
3270
  onMemoryClick: MessageTimelineProps["onMemoryClick"];
2151
3271
  onReconnect: MessageTimelineProps["onReconnect"];
3272
+ renderAuthNeeded: MessageTimelineProps["renderAuthNeeded"];
2152
3273
  resolveProviderLogo: MessageTimelineProps["resolveProviderLogo"];
2153
3274
  toolRegistry: ToolRegistry;
2154
3275
  loadRetainedScreenshot: MessageTimelineProps["loadRetainedScreenshot"];
2155
3276
  loadRetainedArtifact: MessageTimelineProps["loadRetainedArtifact"];
2156
3277
  loadVideoArtifactPlayback: MessageTimelineProps["loadVideoArtifactPlayback"];
3278
+ genieLoading: MessageTimelineProps["genieLoading"];
2157
3279
  turnSummary: MessageTimelineProps["turnSummary"];
2158
3280
  };
2159
3281
 
@@ -2166,10 +3288,13 @@ const TimelineGroupEntry = memo(function TimelineGroupEntry({
2166
3288
  groupKey,
2167
3289
  group,
2168
3290
  nextGroup,
3291
+ startupDismissed,
2169
3292
  entranceEnabled,
2170
3293
  liveEntranceEnabled,
3294
+ noteFolded,
2171
3295
  context,
2172
3296
  }: TimelineGroupEntryProps) {
3297
+ const reducedMotion = useReducedMotion();
2173
3298
  const { behavior, userMessageDisclosureContext } = context;
2174
3299
  const contextCompactionCount =
2175
3300
  group.kind === "turn"
@@ -2180,38 +3305,91 @@ const TimelineGroupEntry = memo(function TimelineGroupEntry({
2180
3305
  nextGroup.item.phase === "compacted"
2181
3306
  ? 1
2182
3307
  : 0;
2183
- return (
2184
- <div data-og-timeline-group-anchor="" data-og-group-key={groupKey}>
2185
- <EntranceAnimationProvider value={entranceEnabled} liveValue={liveEntranceEnabled}>
2186
- <TimelineGroupRenderBoundary resetKeys={[group, behavior]}>
2187
- <UserMessageDisclosureProvider value={userMessageDisclosureContext}>
2188
- <TimelineGroupView
2189
- {...behavior}
2190
- group={group}
2191
- foldLiveCluster={isAgentProgress(nextGroup)}
2192
- trailingAgentText={trailingAgentTextAfterTurn(group, nextGroup)}
2193
- contextCompactionCount={
2194
- contextCompactionCount > 0 ? contextCompactionCount : undefined
2195
- }
2196
- />
2197
- </UserMessageDisclosureProvider>
2198
- </TimelineGroupRenderBoundary>
2199
- </EntranceAnimationProvider>
2200
- </div>
3308
+ const content = (
3309
+ <ActivityNoteTextContext.Provider value={behavior.renderMessageText ?? null}>
3310
+ <TimelineGroupView
3311
+ {...behavior}
3312
+ group={group}
3313
+ foldLiveCluster={isAgentProgress(nextGroup)}
3314
+ startupDismissed={startupDismissed}
3315
+ trailingAgentText={trailingAgentTextAfterTurn(group, nextGroup)}
3316
+ contextCompactionCount={contextCompactionCount > 0 ? contextCompactionCount : undefined}
3317
+ />
3318
+ </ActivityNoteTextContext.Provider>
2201
3319
  );
2202
- });
2203
-
2204
- // The full loaded window stays mounted, so settled history rows must be cheap
2205
- // on every commit: the stable-key projection reuses render-equivalent group
3320
+ return (
3321
+ <GenieLoadingOptionsContext.Provider value={behavior.genieLoading}>
3322
+ <div
3323
+ data-og-timeline-group-anchor=""
3324
+ data-og-group-key={groupKey}
3325
+ data-og-prompt={
3326
+ group.kind === "item" && group.item.kind === "user-message" ? "" : undefined
3327
+ }
3328
+ tabIndex={group.kind === "item" && group.item.kind === "user-message" ? -1 : undefined}
3329
+ // A live progress note reads inside its open work row instead; folded,
3330
+ // it takes no space and also gives back the list's gap-5 above it.
3331
+ data-og-live-note={noteFolded === undefined ? undefined : noteFolded ? "folded" : "shown"}
3332
+ data-og-live-note-id={
3333
+ noteFolded !== undefined && group.kind === "item" ? group.item.id : undefined
3334
+ }
3335
+ aria-hidden={noteFolded || undefined}
3336
+ inert={noteFolded || undefined}
3337
+ className={
3338
+ noteFolded ? "-mt-5 h-0 overflow-hidden opacity-0 [overflow-anchor:none]" : undefined
3339
+ }
3340
+ >
3341
+ <EntranceAnimationProvider value={entranceEnabled} liveValue={liveEntranceEnabled}>
3342
+ <TimelineGroupRenderBoundary resetKeys={[group, behavior]}>
3343
+ <UserMessageDisclosureProvider value={userMessageDisclosureContext}>
3344
+ {/* Item groups never switch the preparation/content key. Keep their
3345
+ DOM shell without mounting inert presence and motion lifecycles
3346
+ for every historical message in a prepend. */}
3347
+ {group.kind === "item" ? (
3348
+ <div>{content}</div>
3349
+ ) : (
3350
+ <AnimatePresence initial={false}>
3351
+ <motion.div
3352
+ key={
3353
+ !startupDismissed &&
3354
+ group.kind === "activity" &&
3355
+ !group.work &&
3356
+ group.items.every(
3357
+ (item) =>
3358
+ item.kind === "startup-phase" ||
3359
+ (item.kind === "reasoning" && !item.text.trim()),
3360
+ )
3361
+ ? "preparation"
3362
+ : "content"
3363
+ }
3364
+ initial={false}
3365
+ exit={{ opacity: 0, height: 0 }}
3366
+ transition={{ duration: reducedMotion ? 0 : 0.2 }}
3367
+ >
3368
+ {content}
3369
+ </motion.div>
3370
+ </AnimatePresence>
3371
+ )}
3372
+ </UserMessageDisclosureProvider>
3373
+ </TimelineGroupRenderBoundary>
3374
+ </EntranceAnimationProvider>
3375
+ </div>
3376
+ </GenieLoadingOptionsContext.Provider>
3377
+ );
3378
+ });
3379
+
3380
+ // The full loaded window stays mounted, so settled history rows must be cheap
3381
+ // on every commit: the stable-key projection reuses render-equivalent group
2206
3382
  // objects, so memo skips them. Changed projection content receives a new group
2207
3383
  // object, and behavior/callback changes are separate props, so ordinary
2208
3384
  // streaming and host updates still invalidate immediately.
2209
3385
  const TimelineGroupView = memo(function TimelineGroupView({
2210
3386
  group,
3387
+ renderMessageActions,
2211
3388
  renderMessageText,
2212
3389
  onOpenSession,
2213
3390
  onMemoryClick,
2214
3391
  onReconnect,
3392
+ renderAuthNeeded,
2215
3393
  resolveProviderLogo,
2216
3394
  toolRegistry,
2217
3395
  loadRetainedScreenshot,
@@ -2221,16 +3399,20 @@ const TimelineGroupView = memo(function TimelineGroupView({
2221
3399
  insideTurn = false,
2222
3400
  nestClusterChips = false,
2223
3401
  foldLiveCluster = false,
3402
+ startupDismissed = false,
2224
3403
  trailingAgentText,
2225
3404
  contextCompactionCount,
2226
3405
  }: {
2227
3406
  group: TimelineGroup;
3407
+ renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
2228
3408
  renderMessageText?:
2229
3409
  | ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
2230
3410
  | undefined;
2231
3411
  onOpenSession?: ((sessionId: string) => void) | undefined;
2232
3412
  onMemoryClick?: ((memoryId: string) => void) | undefined;
2233
3413
  onReconnect?: ((item: AuthNeededItem) => void | Promise<void>) | undefined;
3414
+ /** Host-owned inline connection setup. Return undefined to use the default recovery card. */
3415
+ renderAuthNeeded?: ((item: AuthNeededItem) => ReactNode | undefined) | undefined;
2234
3416
  resolveProviderLogo?: ((providerDomain: string) => string | null | undefined) | undefined;
2235
3417
  toolRegistry: ToolRegistry;
2236
3418
  loadRetainedScreenshot?: RetainedScreenshotLoader | undefined;
@@ -2241,6 +3423,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
2241
3423
  behind a neutral chip — the one place activity without an outcome still
2242
3424
  folds, bounding the DOM of days-long autonomous turns. */
2243
3425
  foldLiveCluster?: boolean;
3426
+ startupDismissed?: boolean;
2244
3427
  /** Rendering inside an expanded turn group: the outer chip already owns the
2245
3428
  failure surface, so nested chips stay tinted but quiet (no repeated
2246
3429
  failure text, no auto-open) — one loud error, N calm sub-expands. */
@@ -2261,15 +3444,29 @@ const TimelineGroupView = memo(function TimelineGroupView({
2261
3444
  /** Secondary chip facet when this fold sits next to a compaction landmark. */
2262
3445
  contextCompactionCount?: number | undefined;
2263
3446
  }) {
3447
+ const startupDetails = useStartupDetails();
2264
3448
  const enter = useEntranceAnimation();
2265
3449
  const settleChrome = useTurnSettleOpen();
3450
+ const previousSingleActivity = useRef<ActivityItem | undefined>(undefined);
3451
+ useLayoutEffect(() => {
3452
+ const work =
3453
+ group.kind === "activity" ? group.items.filter((item) => item.kind !== "startup-phase") : [];
3454
+ previousSingleActivity.current = work.length === 1 ? work[0] : undefined;
3455
+ }, [group]);
2266
3456
  const foldMemory = useFoldMemory();
3457
+ const reportLiveFold = useContext(LiveNoteFoldContext);
2267
3458
  // Settled (or live-fold) activity clusters get a chip. Inside an expanded
2268
3459
  // turn that is the second layer — quiet nested chips under the outer turn
2269
3460
  // summary when contiguous activity naturally clusters (≥2 only).
3461
+ const containsPresentedImage = timelineGroupContainsPresentedImage(group);
3462
+ const hasRememberedImageFold =
3463
+ group.kind === "activity" && containsPresentedImage && foldMemory?.has(group.id);
3464
+ // Primary images stay visible through live narration and the turn wrap.
3465
+ // Manual collapse still belongs to TurnSummary's existing fold memory.
2270
3466
  const activityShouldFold =
2271
- group.kind === "activity" && !!(group.outcome || (foldLiveCluster && clusterIsSettled(group)));
2272
- const containsGeneratedImage = timelineGroupContainsGeneratedImage(group);
3467
+ group.kind === "activity" &&
3468
+ !containsPresentedImage &&
3469
+ !!(group.outcome || (foldLiveCluster && clusterIsSettled(group)));
2273
3470
  // Latch live→folded so a top-level shell that was already mounted open can
2274
3471
  // start the settle beat without remounting bare rail → wrapper.
2275
3472
  const liveActivitySettle = useLiveSettleFold(activityShouldFold && !insideTurn);
@@ -2278,7 +3475,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
2278
3475
  group.kind === "turn" &&
2279
3476
  (group.outcome === "failed" ||
2280
3477
  timelineGroupContainsAuthNeeded(group) ||
2281
- containsGeneratedImage);
3478
+ containsPresentedImage);
2282
3479
  // activity-* → turn-* remount: carry resting state so settleFold does not
2283
3480
  // re-open a chip the reader already watched collapse.
2284
3481
  if (group.kind === "turn" && foldMemory && !insideTurn) {
@@ -2289,9 +3486,167 @@ const TimelineGroupView = memo(function TimelineGroupView({
2289
3486
  );
2290
3487
  }
2291
3488
  const settleFold =
2292
- group.kind === "turn" ? !!(enter && !insideTurn && !turnDefaultOpen) : liveActivitySettle;
3489
+ !turnSummary?.rolling &&
3490
+ (group.kind === "turn" ? !!(enter && !insideTurn && !turnDefaultOpen) : liveActivitySettle);
2293
3491
  switch (group.kind) {
2294
3492
  case "activity":
3493
+ if (group.work) {
3494
+ const preparing = isPreparingWork(group, { startupDetails, startupDismissed });
3495
+ if (preparing) {
3496
+ // Preparation is primary, never hidden in an activity disclosure.
3497
+ // Keep work.startedAt unchanged for the handoff and settled duration.
3498
+ return (
3499
+ <ActivityRail
3500
+ items={group.items}
3501
+ startupActive
3502
+ bare
3503
+ toolRegistry={toolRegistry}
3504
+ onOpenSession={onOpenSession}
3505
+ onMemoryClick={onMemoryClick}
3506
+ loadRetainedScreenshot={loadRetainedScreenshot}
3507
+ loadRetainedArtifact={loadRetainedArtifact}
3508
+ />
3509
+ );
3510
+ }
3511
+ const workStatus = readableWorkStatus({ ...group, work: group.work });
3512
+ const status: TurnSummaryStatus =
3513
+ workStatus.kind === "working"
3514
+ ? {
3515
+ ...workStatus,
3516
+ preview: readableWorkShowsPreview(group) ? (
3517
+ <RollingActivity
3518
+ items={group.items}
3519
+ toolRegistry={toolRegistry}
3520
+ showCount={false}
3521
+ />
3522
+ ) : undefined,
3523
+ }
3524
+ : workStatus.kind === "waiting"
3525
+ ? {
3526
+ kind: "waiting",
3527
+ label: workStatus.label,
3528
+ since: workStatus.since,
3529
+ // One quiet line under the row saying what the wait needs,
3530
+ // kept visible when the work is expanded; never a second
3531
+ // warning banner.
3532
+ guidance: workStatus.detail ? (
3533
+ <span
3534
+ data-og-exchange-wait-detail=""
3535
+ className="block text-og-sm text-og-fg-subtle"
3536
+ >
3537
+ {workStatus.detail}
3538
+ </span>
3539
+ ) : undefined,
3540
+ }
3541
+ : group.work.cycles?.summary
3542
+ ? {
3543
+ ...workStatus,
3544
+ // Folded quiet cycles say what the agent is waiting on.
3545
+ preview: (
3546
+ <p
3547
+ data-og-cycles-summary=""
3548
+ className="truncate text-og-sm text-og-fg-subtle"
3549
+ title={group.work.cycles.summary}
3550
+ >
3551
+ {group.work.cycles.summary}
3552
+ </p>
3553
+ ),
3554
+ }
3555
+ : workStatus;
3556
+ return (
3557
+ // Marks this work row for the live-note fold (list keys can be carried
3558
+ // over from an earlier row, so they are not the work id).
3559
+ <div className="contents" data-og-work-id={group.id}>
3560
+ <TurnSummary
3561
+ key="work"
3562
+ items={group.items}
3563
+ bare={insideTurn || undefined}
3564
+ status={status}
3565
+ // A wait before any visible work has nothing to reveal.
3566
+ expandable={readableWorkHasDetails(
3567
+ { ...group, work: group.work },
3568
+ { startupDetails },
3569
+ )}
3570
+ outcome={group.outcome}
3571
+ failureText={group.failureText}
3572
+ foldKey={group.id}
3573
+ defaultOpen={readableWorkDefaultOpen(group)}
3574
+ facets={turnSummary?.facets}
3575
+ contextCompactionCount={compactedLandmarkCount(group.work.details)}
3576
+ onOpenStateChange={
3577
+ group.work.liveNoteIds?.length && reportLiveFold
3578
+ ? (open) => reportLiveFold(group.id, open)
3579
+ : undefined
3580
+ }
3581
+ >
3582
+ <TurnRailFrame compact>
3583
+ {renderFoldedGroups(
3584
+ group.work.details,
3585
+ {
3586
+ renderMessageActions,
3587
+ renderMessageText,
3588
+ onOpenSession,
3589
+ onMemoryClick,
3590
+ onReconnect,
3591
+ renderAuthNeeded,
3592
+ resolveProviderLogo,
3593
+ toolRegistry,
3594
+ loadRetainedScreenshot,
3595
+ loadRetainedArtifact,
3596
+ loadVideoArtifactPlayback,
3597
+ turnSummary,
3598
+ },
3599
+ true,
3600
+ )}
3601
+ </TurnRailFrame>
3602
+ </TurnSummary>
3603
+ </div>
3604
+ );
3605
+ }
3606
+ // Preparation is one quiet surface, not a fold with eight technical steps.
3607
+ // A later turn of the same exchange keeps its status row instead.
3608
+ if (
3609
+ !startupDetails &&
3610
+ !insideTurn &&
3611
+ !group.outcome &&
3612
+ group.items.every(
3613
+ (item) =>
3614
+ item.kind === "startup-phase" || (item.kind === "reasoning" && !item.text.trim()),
3615
+ )
3616
+ ) {
3617
+ return (
3618
+ <ActivityRail
3619
+ items={group.items}
3620
+ startupActive={!startupDismissed && !foldLiveCluster}
3621
+ bare
3622
+ toolRegistry={toolRegistry}
3623
+ onOpenSession={onOpenSession}
3624
+ onMemoryClick={onMemoryClick}
3625
+ loadRetainedScreenshot={loadRetainedScreenshot}
3626
+ loadRetainedArtifact={loadRetainedArtifact}
3627
+ />
3628
+ );
3629
+ }
3630
+ if (
3631
+ turnSummary?.rolling &&
3632
+ insideTurn &&
3633
+ !startupDetails &&
3634
+ !hasRememberedImageFold &&
3635
+ group.items.filter((item) => item.kind !== "startup-phase").length === 1
3636
+ ) {
3637
+ return (
3638
+ <ActivityRail
3639
+ items={group.items}
3640
+ startupActive={false}
3641
+ bare
3642
+ toolRegistry={toolRegistry}
3643
+ onOpenSession={onOpenSession}
3644
+ onMemoryClick={onMemoryClick}
3645
+ loadRetainedScreenshot={loadRetainedScreenshot}
3646
+ loadRetainedArtifact={loadRetainedArtifact}
3647
+ />
3648
+ );
3649
+ }
2295
3650
  if (insideTurn) {
2296
3651
  // Nested chips whenever the parent has ≥2 clusters. During outer settle
2297
3652
  // chrome they stay force-open so structure is visible and height stays
@@ -2303,11 +3658,22 @@ const TimelineGroupView = memo(function TimelineGroupView({
2303
3658
  // settled closed pre-wrap was showing as a CHIP, so mounting it closed
2304
3659
  // is both the stable height and the honest state — force-opening it
2305
3660
  // was the "already-collapsed cluster auto-expands at the end" reopen.
2306
- const useNestedChip = nestClusterChips && activityShouldFold && !containsGeneratedImage;
3661
+ const visibleActivity = group.items.filter((item) => item.kind !== "startup-phase");
3662
+ const singleThought =
3663
+ !startupDetails &&
3664
+ visibleActivity.length === 1 &&
3665
+ visibleActivity[0]?.kind === "reasoning";
3666
+ // Untouched images stay primary output, but a reader-owned image fold
3667
+ // must keep its shell across a multi-cluster wrap. A bare rail would
3668
+ // bypass the remembered choice; retaining either state also lets the
3669
+ // reader reopen and close the same chip after settlement.
3670
+ const useNestedChip =
3671
+ nestClusterChips && (activityShouldFold || hasRememberedImageFold) && !singleThought;
2307
3672
  if (!useNestedChip) {
2308
3673
  return (
2309
3674
  <ActivityRail
2310
3675
  items={group.items}
3676
+ {...(startupDismissed ? { startupActive: false } : {})}
2311
3677
  onOpenSession={onOpenSession}
2312
3678
  onMemoryClick={onMemoryClick}
2313
3679
  toolRegistry={toolRegistry}
@@ -2345,10 +3711,31 @@ const TimelineGroupView = memo(function TimelineGroupView({
2345
3711
  // flips settleFold (collapse) instead of remounting bare rail → wrapper.
2346
3712
  return (
2347
3713
  <TurnSummary
3714
+ // Apply the primary-output default when a rolling activity first
3715
+ // produces an image; the stable foldKey still preserves user choices.
3716
+ key={containsPresentedImage ? "primary-image" : "activity"}
2348
3717
  items={group.items}
2349
3718
  outcome={group.outcome}
2350
3719
  failureText={group.failureText}
2351
- defaultOpen={!activityShouldFold || group.outcome === "failed" ? true : undefined}
3720
+ defaultOpen={
3721
+ group.outcome === "failed" ||
3722
+ containsPresentedImage ||
3723
+ (!turnSummary?.rolling && !activityShouldFold)
3724
+ ? true
3725
+ : undefined
3726
+ }
3727
+ liveHeader={
3728
+ turnSummary?.rolling &&
3729
+ !containsPresentedImage &&
3730
+ !group.outcome &&
3731
+ !foldLiveCluster ? (
3732
+ <RollingActivity
3733
+ items={group.items}
3734
+ toolRegistry={toolRegistry}
3735
+ previousItem={previousSingleActivity.current}
3736
+ />
3737
+ ) : undefined
3738
+ }
2352
3739
  foldKey={group.id}
2353
3740
  facets={turnSummary?.facets}
2354
3741
  settleFold={settleFold}
@@ -2371,54 +3758,40 @@ const TimelineGroupView = memo(function TimelineGroupView({
2371
3758
  );
2372
3759
  case "turn": {
2373
3760
  const activityItems = flattenActivityItems(group.groups);
2374
- // Second-layer chips only when there are natural multi-cluster seams —
2375
- // otherwise the outer turn chip alone is enough ("N steps" wrapping one
2376
- // more "N steps" was the redundant double fold).
2377
- const nestClusters = foldableActivityClusterCount(group.groups) >= 2;
3761
+ if (
3762
+ !startupDetails &&
3763
+ group.outcome === "complete" &&
3764
+ group.groups.every(
3765
+ (child) =>
3766
+ child.kind === "activity" &&
3767
+ child.items.every(
3768
+ (item) => item.kind === "startup-phase" && item.status === "complete",
3769
+ ),
3770
+ )
3771
+ )
3772
+ return <ActivityRail items={activityItems} startupActive={false} bare />;
2378
3773
  const turnCopyText = collectTurnCopyText(group.groups, trailingAgentText);
2379
- const body = group.groups.map((child) => {
2380
- const key = timelineGroupKey(child);
2381
- return (
2382
- <TimelineGroupRenderBoundary
2383
- key={key}
2384
- resetKeys={[
2385
- child,
2386
- renderMessageText,
2387
- onOpenSession,
2388
- onMemoryClick,
2389
- onReconnect,
2390
- resolveProviderLogo,
2391
- toolRegistry,
2392
- loadRetainedScreenshot,
2393
- loadRetainedArtifact,
2394
- loadVideoArtifactPlayback,
2395
- turnSummary,
2396
- ]}
2397
- >
2398
- <TimelineGroupView
2399
- group={child}
2400
- renderMessageText={renderMessageText}
2401
- onOpenSession={onOpenSession}
2402
- onMemoryClick={onMemoryClick}
2403
- onReconnect={onReconnect}
2404
- resolveProviderLogo={resolveProviderLogo}
2405
- toolRegistry={toolRegistry}
2406
- loadRetainedScreenshot={loadRetainedScreenshot}
2407
- loadRetainedArtifact={loadRetainedArtifact}
2408
- loadVideoArtifactPlayback={loadVideoArtifactPlayback}
2409
- turnSummary={turnSummary}
2410
- insideTurn
2411
- nestClusterChips={nestClusters}
2412
- />
2413
- </TimelineGroupRenderBoundary>
2414
- );
3774
+ const body = renderFoldedGroups(group.groups, {
3775
+ renderMessageActions,
3776
+ renderMessageText,
3777
+ onOpenSession,
3778
+ onMemoryClick,
3779
+ onReconnect,
3780
+ renderAuthNeeded,
3781
+ resolveProviderLogo,
3782
+ toolRegistry,
3783
+ loadRetainedScreenshot,
3784
+ loadRetainedArtifact,
3785
+ loadVideoArtifactPlayback,
3786
+ turnSummary,
2415
3787
  });
3788
+ const durationMs = durationBetween(group.startedAt, group.endedAt);
2416
3789
  return (
2417
3790
  <TurnSummary
2418
3791
  items={activityItems}
2419
3792
  outcome={group.outcome}
2420
3793
  failureText={insideTurn ? undefined : group.failureText}
2421
- durationMs={durationBetween(group.startedAt, group.endedAt)}
3794
+ durationMs={durationMs}
2422
3795
  defaultOpen={turnDefaultOpen ? true : undefined}
2423
3796
  bare={insideTurn}
2424
3797
  foldKey={group.id}
@@ -2443,8 +3816,10 @@ const TimelineGroupView = memo(function TimelineGroupView({
2443
3816
  return (
2444
3817
  <TimelineRow
2445
3818
  item={group.item}
3819
+ renderMessageActions={renderMessageActions}
2446
3820
  renderMessageText={renderMessageText}
2447
3821
  onReconnect={onReconnect}
3822
+ renderAuthNeeded={renderAuthNeeded}
2448
3823
  resolveProviderLogo={resolveProviderLogo}
2449
3824
  onOpenSession={onOpenSession}
2450
3825
  loadVideoArtifactPlayback={loadVideoArtifactPlayback}
@@ -2453,6 +3828,94 @@ const TimelineGroupView = memo(function TimelineGroupView({
2453
3828
  }
2454
3829
  });
2455
3830
 
3831
+ type FoldedGroupBehavior = {
3832
+ renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
3833
+ renderMessageText?:
3834
+ | ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
3835
+ | undefined;
3836
+ onOpenSession?: ((sessionId: string) => void) | undefined;
3837
+ onMemoryClick?: ((memoryId: string) => void) | undefined;
3838
+ onReconnect?: ((item: AuthNeededItem) => void | Promise<void>) | undefined;
3839
+ renderAuthNeeded?: ((item: AuthNeededItem) => ReactNode | undefined) | undefined;
3840
+ resolveProviderLogo?: ((providerDomain: string) => string | null | undefined) | undefined;
3841
+ toolRegistry: ToolRegistry;
3842
+ loadRetainedScreenshot?: RetainedScreenshotLoader | undefined;
3843
+ loadRetainedArtifact?: RetainedArtifactLoader | undefined;
3844
+ loadVideoArtifactPlayback?: VideoArtifactPlaybackLoader | undefined;
3845
+ turnSummary?: TurnSummaryOptions | undefined;
3846
+ };
3847
+
3848
+ /**
3849
+ * A search hit inside a nested work row opens that row only. The reveal key
3850
+ * is provided per top-level group, so without narrowing every nested row of
3851
+ * a folded quiet-cycle run would open at once.
3852
+ */
3853
+ function NestedSearchReveal({ group, children }: { group: TimelineGroup; children: ReactNode }) {
3854
+ const revealKey = useContext(TimelineSearchRevealContext);
3855
+ const itemId = useMemo(() => {
3856
+ if (!revealKey) return null;
3857
+ try {
3858
+ const [id] = JSON.parse(revealKey) as unknown[];
3859
+ return typeof id === "string" ? id : null;
3860
+ } catch {
3861
+ return null;
3862
+ }
3863
+ }, [revealKey]);
3864
+ const value = itemId && timelineGroupItemIds(group).includes(itemId) ? revealKey : null;
3865
+ return (
3866
+ <TimelineSearchRevealContext.Provider value={value}>
3867
+ {children}
3868
+ </TimelineSearchRevealContext.Provider>
3869
+ );
3870
+ }
3871
+
3872
+ function renderFoldedGroups(
3873
+ groups: readonly TimelineGroup[],
3874
+ behavior: FoldedGroupBehavior,
3875
+ readableWork = false,
3876
+ ) {
3877
+ // Second-layer chips only when there are natural multi-cluster seams:
3878
+ // otherwise the outer turn chip alone is enough ("N steps" wrapping one
3879
+ // more "N steps" was the redundant double fold).
3880
+ const nestClusters = foldableActivityClusterCount(groups) >= 2;
3881
+ return groups.map((child, index) => (
3882
+ <TimelineGroupRenderBoundary
3883
+ key={timelineGroupKey(child)}
3884
+ resetKeys={[
3885
+ child,
3886
+ behavior.renderMessageActions,
3887
+ behavior.renderMessageText,
3888
+ behavior.onOpenSession,
3889
+ behavior.onMemoryClick,
3890
+ behavior.onReconnect,
3891
+ behavior.renderAuthNeeded,
3892
+ behavior.resolveProviderLogo,
3893
+ behavior.toolRegistry,
3894
+ behavior.loadRetainedScreenshot,
3895
+ behavior.loadRetainedArtifact,
3896
+ behavior.loadVideoArtifactPlayback,
3897
+ behavior.turnSummary,
3898
+ ]}
3899
+ >
3900
+ {readableWork ? (
3901
+ <div
3902
+ className={
3903
+ index > 0 && (child.kind !== "activity" || groups[index - 1]?.kind !== "activity")
3904
+ ? "mt-3"
3905
+ : undefined
3906
+ }
3907
+ >
3908
+ <NestedSearchReveal group={child}>
3909
+ <TimelineGroupView {...behavior} group={child} insideTurn startupDismissed />
3910
+ </NestedSearchReveal>
3911
+ </div>
3912
+ ) : (
3913
+ <TimelineGroupView {...behavior} group={child} insideTurn nestClusterChips={nestClusters} />
3914
+ )}
3915
+ </TimelineGroupRenderBoundary>
3916
+ ));
3917
+ }
3918
+
2456
3919
  /**
2457
3920
  * Body under a turn/activity chip. Remount flashes are gated by the timeline
2458
3921
  * seen-activity-id map (not by killing entrance): FoldBody used to force
@@ -2464,9 +3927,16 @@ function FoldBody({ children }: { children: ReactNode }) {
2464
3927
 
2465
3928
  /** Stable left rule for turn/activity bodies — always present so settle wrap
2466
3929
  never inserts or removes the rail chrome. */
2467
- function TurnRailFrame({ children }: { children: ReactNode }) {
3930
+ function TurnRailFrame({ children, compact = false }: { children: ReactNode; compact?: boolean }) {
2468
3931
  return (
2469
- <div className="flex flex-col gap-4 border-l-2 border-og-border pl-3 sm:pl-4">{children}</div>
3932
+ <div
3933
+ className={cn(
3934
+ "flex flex-col border-l-2 border-og-border pl-3 sm:pl-4",
3935
+ compact ? "gap-0.5" : "gap-4",
3936
+ )}
3937
+ >
3938
+ {children}
3939
+ </div>
2470
3940
  );
2471
3941
  }
2472
3942
 
@@ -2513,26 +3983,15 @@ function timelineGroupContainsAuthNeeded(group: TimelineGroup): boolean {
2513
3983
  }
2514
3984
  }
2515
3985
 
2516
- /** Generated images are primary user-visible output, not incidental activity. */
2517
- function timelineGroupContainsGeneratedImage(group: TimelineGroup): boolean {
2518
- switch (group.kind) {
2519
- case "item":
2520
- return false;
2521
- case "activity":
2522
- return group.items.some(
2523
- (item) => item.kind === "tool-call" && item.name === "generate_image",
2524
- );
2525
- case "turn":
2526
- return group.groups.some(timelineGroupContainsGeneratedImage);
2527
- }
2528
- }
2529
-
2530
3986
  function timelineGroupItemIds(group: TimelineGroup): string[] {
2531
3987
  switch (group.kind) {
2532
3988
  case "item":
2533
3989
  return [group.item.id];
2534
3990
  case "activity":
2535
- return group.items.map((item) => item.id);
3991
+ return [
3992
+ ...(group.work?.details ?? []).flatMap(timelineGroupItemIds),
3993
+ ...group.items.map((item) => item.id),
3994
+ ];
2536
3995
  case "turn":
2537
3996
  return group.groups.flatMap(timelineGroupItemIds);
2538
3997
  }
@@ -2550,25 +4009,13 @@ function isAgentProgress(next: TimelineGroup | undefined): boolean {
2550
4009
  return (
2551
4010
  next.kind === "activity" ||
2552
4011
  next.kind === "turn" ||
2553
- (next.kind === "item" && next.item.kind === "agent-message")
4012
+ (next.kind === "item" && next.item.kind === "agent-message" && next.item.text.trim().length > 0)
2554
4013
  );
2555
4014
  }
2556
4015
 
2557
4016
  /** No item still running or streaming — the only state safe to fold live.
2558
4017
  Position alone is a broken proxy: a pending queued message (or any trailing
2559
4018
  item) can sit after the ACTIVE cluster, which must never fold mid-work. */
2560
- function clusterIsSettled(group: Extract<TimelineGroup, { kind: "activity" }>): boolean {
2561
- return group.items.every((item) => {
2562
- if (item.kind === "reasoning") {
2563
- return !item.streaming;
2564
- }
2565
- // Memory writes and fleet observations are discrete, already-settled events.
2566
- if (item.kind === "memory" || item.kind === "fleet-decision") {
2567
- return true;
2568
- }
2569
- return item.status !== "running";
2570
- });
2571
- }
2572
4019
 
2573
4020
  /** Settled activity clusters that could become nested chips under a turn. */
2574
4021
  function foldableActivityClusterCount(groups: readonly TimelineGroup[]): number {
@@ -2581,18 +4028,6 @@ function foldableActivityClusterCount(groups: readonly TimelineGroup[]): number
2581
4028
  return count;
2582
4029
  }
2583
4030
 
2584
- function flattenActivityItems(groups: TimelineGroup[]): ActivityItem[] {
2585
- const items: ActivityItem[] = [];
2586
- for (const group of groups) {
2587
- if (group.kind === "activity") {
2588
- items.push(...group.items);
2589
- } else if (group.kind === "turn") {
2590
- items.push(...flattenActivityItems(group.groups));
2591
- }
2592
- }
2593
- return items;
2594
- }
2595
-
2596
4031
  /** Assistant prose inside a turn fold (mid-turn narration), joined for copy. */
2597
4032
  function collectAgentMessageText(groups: readonly TimelineGroup[]): string {
2598
4033
  const parts: string[] = [];
@@ -2602,6 +4037,18 @@ function collectAgentMessageText(groups: readonly TimelineGroup[]): string {
2602
4037
  if (text.length > 0) {
2603
4038
  parts.push(text);
2604
4039
  }
4040
+ } else if (group.kind === "activity") {
4041
+ // Commentary folded into clusters is still the assistant's own prose.
4042
+ const nested = collectAgentMessageText(group.work?.details ?? []);
4043
+ if (nested.length > 0) {
4044
+ parts.push(nested);
4045
+ }
4046
+ for (const item of group.items) {
4047
+ const text = item.kind === "agent-message" ? item.text.trim() : "";
4048
+ if (text.length > 0) {
4049
+ parts.push(text);
4050
+ }
4051
+ }
2605
4052
  } else if (group.kind === "turn") {
2606
4053
  const nested = collectAgentMessageText(group.groups);
2607
4054
  if (nested.length > 0) {
@@ -2622,6 +4069,9 @@ function collectTurnIdsFromGroups(groups: readonly TimelineGroup[]): Set<string>
2622
4069
  ids.add(turnId);
2623
4070
  }
2624
4071
  } else if (group.kind === "activity") {
4072
+ for (const nested of collectTurnIdsFromGroups(group.work?.details ?? [])) {
4073
+ ids.add(nested);
4074
+ }
2625
4075
  for (const item of group.items) {
2626
4076
  if (item.turnId) {
2627
4077
  ids.add(item.turnId);
@@ -2676,15 +4126,6 @@ function collectTurnCopyText(
2676
4126
  return parts.join("\n\n");
2677
4127
  }
2678
4128
 
2679
- function durationBetween(startedAt: string, endedAt: string): number | undefined {
2680
- const started = Date.parse(startedAt);
2681
- const ended = Date.parse(endedAt);
2682
- if (!Number.isFinite(started) || !Number.isFinite(ended) || ended < started) {
2683
- return undefined;
2684
- }
2685
- return ended - started;
2686
- }
2687
-
2688
4129
  /* --- single rows ------------------------------------------------------------ */
2689
4130
 
2690
4131
  /**
@@ -2694,28 +4135,45 @@ function durationBetween(startedAt: string, endedAt: string): number | undefined
2694
4135
  */
2695
4136
  export function TimelineRow({
2696
4137
  item,
4138
+ renderMessageActions,
2697
4139
  renderMessageText,
2698
4140
  onReconnect,
4141
+ renderAuthNeeded,
2699
4142
  resolveProviderLogo,
2700
4143
  onOpenSession,
2701
4144
  loadVideoArtifactPlayback,
2702
4145
  }: {
2703
4146
  item: TimelineItem;
4147
+ renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
2704
4148
  renderMessageText?:
2705
4149
  | ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
2706
4150
  | undefined;
2707
4151
  onReconnect?: ((item: AuthNeededItem) => void | Promise<void>) | undefined;
4152
+ /** Host-owned inline connection setup. Return undefined to use the default recovery card. */
4153
+ renderAuthNeeded?: ((item: AuthNeededItem) => ReactNode | undefined) | undefined;
2708
4154
  resolveProviderLogo?: ((providerDomain: string) => string | null | undefined) | undefined;
2709
4155
  onOpenSession?: ((sessionId: string) => void) | undefined;
2710
4156
  loadVideoArtifactPlayback?: VideoArtifactPlaybackLoader | undefined;
2711
4157
  }) {
2712
4158
  switch (item.kind) {
2713
4159
  case "user-message":
2714
- return <UserMessageRow item={item} renderMessageText={renderMessageText} />;
4160
+ return (
4161
+ <UserMessageRow
4162
+ item={item}
4163
+ renderMessageActions={renderMessageActions}
4164
+ renderMessageText={renderMessageText}
4165
+ />
4166
+ );
2715
4167
  case "human-input":
2716
4168
  return <HumanInputConversationRow item={item} />;
2717
4169
  case "agent-message":
2718
- return <AgentMessageRow item={item} renderMessageText={renderMessageText} />;
4170
+ return (
4171
+ <AgentMessageRow
4172
+ item={item}
4173
+ renderMessageActions={renderMessageActions}
4174
+ renderMessageText={renderMessageText}
4175
+ />
4176
+ );
2719
4177
  case "worker-completion":
2720
4178
  return <WorkerCompletionRow item={item} onOpenSession={onOpenSession} />;
2721
4179
  case "session-status":
@@ -2724,7 +4182,11 @@ export function TimelineRow({
2724
4182
  return <GoalRow item={item} />;
2725
4183
  case "machine-input-batch":
2726
4184
  return (
2727
- <MachineInputBatchRow item={item} loadVideoArtifactPlayback={loadVideoArtifactPlayback} />
4185
+ <MachineInputBatchRow
4186
+ item={item}
4187
+ onOpenSession={onOpenSession}
4188
+ loadVideoArtifactPlayback={loadVideoArtifactPlayback}
4189
+ />
2728
4190
  );
2729
4191
  case "notice":
2730
4192
  return <NoticeRow item={item} />;
@@ -2732,11 +4194,13 @@ export function TimelineRow({
2732
4194
  return <CompactionRow item={item} />;
2733
4195
  case "auth-needed":
2734
4196
  return (
2735
- <AuthNeededRow
2736
- item={item}
2737
- onReconnect={onReconnect}
2738
- resolveProviderLogo={resolveProviderLogo}
2739
- />
4197
+ renderAuthNeeded?.(item) ?? (
4198
+ <AuthNeededRow
4199
+ item={item}
4200
+ onReconnect={onReconnect}
4201
+ resolveProviderLogo={resolveProviderLogo}
4202
+ />
4203
+ )
2740
4204
  );
2741
4205
  default:
2742
4206
  return null;
@@ -2752,6 +4216,8 @@ const COMPACTION_TRIGGER_LABEL: Record<NonNullable<ContextCompactionItem["trigge
2752
4216
 
2753
4217
  function CompactionRow({ item }: { item: ContextCompactionItem }) {
2754
4218
  const enter = useEntranceAnimation();
4219
+ const detailsId = useId();
4220
+ const [detailsOpen, setDetailsOpen] = useState(false);
2755
4221
  const trigger =
2756
4222
  item.trigger && item.phase !== "started" ? COMPACTION_TRIGGER_LABEL[item.trigger] : null;
2757
4223
  const before =
@@ -2769,56 +4235,97 @@ function CompactionRow({ item }: { item: ContextCompactionItem }) {
2769
4235
  ? before && after
2770
4236
  ? `Conversation history compacted · ~${before} → ~${after} estimated history tokens`
2771
4237
  : "Conversation history compacted"
2772
- : "Couldn’t compact conversation history";
4238
+ : compactionSkipTitle(item.skipReason);
2773
4239
  const subtitle =
2774
4240
  item.phase === "compacted"
2775
4241
  ? "Chat history above is unchanged"
2776
4242
  : item.phase === "skipped"
2777
- ? compactionSkipSubtitle(item.skipReason)
4243
+ ? compactionSkipSubtitle(item.skipReason, item.providerRejection)
2778
4244
  : null;
2779
- const pill =
2780
- item.phase === "skipped" && item.skipReason === "summarization_failed"
2781
- ? "border-og-status-failed/35 bg-og-status-failed/10 text-og-status-failed"
2782
- : item.phase === "started"
2783
- ? WAITING_PILL_CLASS
2784
- : "border-og-border bg-og-surface-1 text-og-fg-muted";
4245
+ // Provider identifiers are diagnostics: one short line up front, the rest on demand.
4246
+ const details = compactionFailureDetails(item);
4247
+ const failed = item.phase === "skipped" && item.skipReason === "summarization_failed";
4248
+ const pill = failed
4249
+ ? "border-og-status-failed/35 bg-og-status-failed/10 text-og-status-failed"
4250
+ : item.phase === "started"
4251
+ ? WAITING_PILL_CLASS
4252
+ : NEUTRAL_PILL;
2785
4253
  return (
2786
- <div className={cn(enter && "animate-og-enter", "flex justify-center")}>
4254
+ <div className={cn(enter && "animate-og-enter", "flex min-w-0 flex-col items-center gap-1.5")}>
2787
4255
  <div
2788
4256
  className={cn(
2789
- "inline-flex max-w-full flex-col items-center gap-0.5 rounded-full border px-3 py-1.5 text-og-sm",
4257
+ "inline-flex min-w-0 max-w-full flex-col items-center gap-0.5 border px-3 py-1.5 text-og-sm",
4258
+ // A failure's recovery step may wrap on narrow screens; a full pill
4259
+ // radius would pinch its second line.
4260
+ failed ? "rounded-og-lg" : "rounded-full",
2790
4261
  pill,
2791
4262
  )}
2792
4263
  role="status"
2793
4264
  >
2794
- <span className="inline-flex max-w-full items-center gap-1.5">
4265
+ <span className="inline-flex min-w-0 max-w-full items-center gap-1.5">
2795
4266
  <ShrinkIcon className="size-3.5 shrink-0" />
2796
- <span className="truncate">
4267
+ <span className="min-w-0 truncate">
2797
4268
  {title}
2798
4269
  {trigger ? ` · ${trigger}` : ""}
2799
4270
  </span>
2800
4271
  </span>
2801
- {subtitle ? <span className="truncate text-og-xs opacity-80">{subtitle}</span> : null}
4272
+ {subtitle || details ? (
4273
+ <span
4274
+ className={cn(
4275
+ "min-w-0 max-w-full text-og-xs",
4276
+ failed ? "text-balance text-center" : "inline-flex items-center gap-1",
4277
+ )}
4278
+ >
4279
+ {subtitle ? (
4280
+ <span className={cn("opacity-80", !failed && "min-w-0 truncate")}>{subtitle}</span>
4281
+ ) : null}
4282
+ {subtitle && details && failed ? " " : null}
4283
+ {details ? (
4284
+ <button
4285
+ type="button"
4286
+ aria-expanded={detailsOpen}
4287
+ aria-controls={detailsId}
4288
+ onClick={() => setDetailsOpen((open) => !open)}
4289
+ className={cn(
4290
+ "inline-flex shrink-0 items-center gap-0.5 whitespace-nowrap rounded-og-sm align-baseline font-medium underline-offset-2",
4291
+ "hover:underline focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-current/40",
4292
+ "pointer-coarse:-my-2 pointer-coarse:px-1 pointer-coarse:py-2",
4293
+ )}
4294
+ >
4295
+ Details
4296
+ <ChevronDownIcon
4297
+ aria-hidden
4298
+ className={cn("size-3 transition-transform", detailsOpen && "rotate-180")}
4299
+ />
4300
+ </button>
4301
+ ) : null}
4302
+ </span>
4303
+ ) : null}
2802
4304
  </div>
4305
+ {details ? (
4306
+ <div
4307
+ id={detailsId}
4308
+ hidden={!detailsOpen}
4309
+ data-og-compaction-details=""
4310
+ className="w-full max-w-md rounded-og-md border border-og-border bg-og-surface-1 px-3 py-2 text-left text-og-xs text-og-fg-muted"
4311
+ >
4312
+ {details.note ? <p className="mb-2">{details.note}</p> : null}
4313
+ <dl className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-1">
4314
+ {details.facts.map((fact) => (
4315
+ <Fragment key={fact.label}>
4316
+ <dt className="text-og-fg-subtle">{fact.label}</dt>
4317
+ <dd className="max-h-40 min-w-0 overflow-auto whitespace-pre-wrap break-words font-mono">
4318
+ {fact.value}
4319
+ </dd>
4320
+ </Fragment>
4321
+ ))}
4322
+ </dl>
4323
+ </div>
4324
+ ) : null}
2803
4325
  </div>
2804
4326
  );
2805
4327
  }
2806
4328
 
2807
- function compactionSkipSubtitle(reason: string | null): string {
2808
- switch (reason) {
2809
- case "no_history":
2810
- return "No active history to compact";
2811
- case "replacement_not_smaller":
2812
- return "Checkpoint would not reduce memory size";
2813
- case "replacement_unchanged":
2814
- return "Checkpoint made no progress";
2815
- case "summarization_failed":
2816
- return "Request it again to retry. Chat history is unchanged.";
2817
- default:
2818
- return "Compaction was not needed. Chat history is unchanged.";
2819
- }
2820
- }
2821
-
2822
4329
  /** Hover-reveal clock beside the copy control (sent / finished). */
2823
4330
  function MessageFooterTime({ occurredAt }: { occurredAt: string }) {
2824
4331
  return (
@@ -2837,9 +4344,11 @@ function MessageFooterTime({ occurredAt }: { occurredAt: string }) {
2837
4344
 
2838
4345
  function UserMessageRow({
2839
4346
  item,
4347
+ renderMessageActions,
2840
4348
  renderMessageText,
2841
4349
  }: {
2842
4350
  item: UserMessageItem;
4351
+ renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
2843
4352
  renderMessageText?:
2844
4353
  | ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
2845
4354
  | undefined;
@@ -2849,6 +4358,7 @@ function UserMessageRow({
2849
4358
  return (
2850
4359
  <div className={cn(enter && "animate-og-enter", "flex justify-end")}>
2851
4360
  <div className="flex max-w-[85%] min-w-0 flex-col items-end gap-1">
4361
+ <MessageSenderSlot item={item} />
2852
4362
  <CopyHoverFrame
2853
4363
  copyText={
2854
4364
  item.text ||
@@ -2858,22 +4368,30 @@ function UserMessageRow({
2858
4368
  }
2859
4369
  label="Copy message"
2860
4370
  className="w-fit max-w-full min-w-0"
2861
- trailing={<MessageFooterTime occurredAt={item.occurredAt} />}
4371
+ trailing={
4372
+ <>
4373
+ {renderMessageActions?.(item)}
4374
+ <MessageFooterTime occurredAt={item.occurredAt} />
4375
+ </>
4376
+ }
2862
4377
  >
2863
4378
  <div className={MESSAGE_BUBBLE_CLASS}>
2864
4379
  {item.text ? (
2865
- <div data-og-annotation-source-key={item.annotationSource?.eventId}>
4380
+ <div
4381
+ data-og-search-item={item.id}
4382
+ data-og-annotation-source-key={item.annotationSource?.eventId}
4383
+ >
2866
4384
  {renderMessageText ? (
2867
4385
  renderMessageText(item.text, item)
2868
4386
  ) : (
2869
4387
  <UserMessageBody messageId={item.id} text={item.text}>
2870
- <Markdown>{item.text}</Markdown>
4388
+ <Markdown softLineBreaks>{item.text}</Markdown>
2871
4389
  </UserMessageBody>
2872
4390
  )}
2873
4391
  </div>
2874
4392
  ) : null}
2875
4393
  {(item.annotations?.length ?? 0) > 0 ? (
2876
- <TimelineAnnotationsChip
4394
+ <TimelineAnnotationCards
2877
4395
  annotations={item.annotations ?? []}
2878
4396
  className={item.text ? "mt-2" : undefined}
2879
4397
  />
@@ -2883,30 +4401,43 @@ function UserMessageRow({
2883
4401
  {deliveryFailed ? (
2884
4402
  <div
2885
4403
  role="status"
2886
- className="flex max-w-full items-center gap-2 px-1 text-og-xs text-og-status-failed"
4404
+ className="flex max-w-full flex-col items-start gap-1 px-1 text-og-xs text-og-status-failed"
2887
4405
  >
2888
- <span className="inline-flex items-center gap-1" title={item.delivery?.error}>
4406
+ <span className="flex min-w-0 items-start gap-1">
2889
4407
  <TriangleAlertIcon className="size-3.5 shrink-0" aria-hidden="true" />
2890
- <span>Message not sent</span>
4408
+ <span className="min-w-0 break-words">
4409
+ {item.delivery?.error || "Message not sent"}
4410
+ </span>
2891
4411
  </span>
2892
- {item.delivery?.onRetry ? (
2893
- <button
2894
- type="button"
2895
- className="font-medium text-og-status-failed underline decoration-og-status-failed/50 underline-offset-2 hover:text-og-fg"
2896
- onClick={item.delivery.onRetry}
2897
- >
2898
- Retry
2899
- </button>
2900
- ) : null}
2901
- {item.delivery?.onRemove ? (
2902
- <button
2903
- type="button"
2904
- className="font-medium text-og-fg-subtle underline decoration-og-border underline-offset-2 hover:text-og-fg"
2905
- onClick={item.delivery.onRemove}
2906
- >
2907
- Remove
2908
- </button>
2909
- ) : null}
4412
+ <div className="flex flex-wrap items-center gap-2">
4413
+ {item.delivery?.onEdit ? (
4414
+ <button
4415
+ type="button"
4416
+ className="font-medium text-og-status-failed underline decoration-og-status-failed/50 underline-offset-2 hover:text-og-fg"
4417
+ onClick={item.delivery.onEdit}
4418
+ >
4419
+ Edit message
4420
+ </button>
4421
+ ) : null}
4422
+ {item.delivery?.onRetry ? (
4423
+ <button
4424
+ type="button"
4425
+ className="font-medium text-og-status-failed underline decoration-og-status-failed/50 underline-offset-2 hover:text-og-fg"
4426
+ onClick={item.delivery.onRetry}
4427
+ >
4428
+ Retry
4429
+ </button>
4430
+ ) : null}
4431
+ {item.delivery?.onRemove ? (
4432
+ <button
4433
+ type="button"
4434
+ className="font-medium text-og-fg-subtle underline decoration-og-border underline-offset-2 hover:text-og-fg"
4435
+ onClick={item.delivery.onRemove}
4436
+ >
4437
+ Remove
4438
+ </button>
4439
+ ) : null}
4440
+ </div>
2910
4441
  </div>
2911
4442
  ) : null}
2912
4443
  </div>
@@ -2946,7 +4477,7 @@ function HumanInputConversationRow({ item }: { item: HumanInputItem }) {
2946
4477
  item.questions.map((question, index) => (
2947
4478
  <div key={question.id}>
2948
4479
  {question.label ? (
2949
- <p className="text-og-sm font-semibold text-og-fg">
4480
+ <p className="text-og-sm font-semibold text-og-fg [overflow-wrap:anywhere]">
2950
4481
  {multipleQuestions ? (
2951
4482
  <span className="mr-1.5 tabular-nums text-og-fg-muted">{index + 1}.</span>
2952
4483
  ) : null}
@@ -2956,7 +4487,7 @@ function HumanInputConversationRow({ item }: { item: HumanInputItem }) {
2956
4487
  ) : null}
2957
4488
  <p
2958
4489
  className={cn(
2959
- "text-og-md leading-6 text-og-fg",
4490
+ "text-og-md leading-6 text-og-fg [overflow-wrap:anywhere]",
2960
4491
  question.label && "mt-0.5 text-og-sm text-og-fg-muted",
2961
4492
  )}
2962
4493
  >
@@ -2964,7 +4495,7 @@ function HumanInputConversationRow({ item }: { item: HumanInputItem }) {
2964
4495
  <span className="mr-1.5 tabular-nums text-og-fg-muted">{index + 1}.</span>
2965
4496
  ) : null}
2966
4497
  {!question.label && multipleQuestions ? " " : null}
2967
- {question.prompt}
4498
+ {linkifyText(question.prompt)}
2968
4499
  </p>
2969
4500
  </div>
2970
4501
  ))
@@ -2998,7 +4529,12 @@ function HumanInputConversationRow({ item }: { item: HumanInputItem }) {
2998
4529
  </p>
2999
4530
  ) : null}
3000
4531
  <div className={cn("text-og-md text-og-fg", multipleQuestions && "mt-0.5")}>
3001
- {answer.values.length > 1 ? (
4532
+ {answer.secret ? (
4533
+ <SecretAnswerChip
4534
+ result={answer.secret}
4535
+ label={secretAnswerLabel(answer.secret)}
4536
+ />
4537
+ ) : answer.values.length > 1 ? (
3002
4538
  <ul className="list-disc space-y-0.5 pl-5">
3003
4539
  {answer.values.map((value) => (
3004
4540
  <li key={`${answer.questionId}-${value}`}>{value}</li>
@@ -3039,7 +4575,11 @@ function humanInputConversationCopyText(item: HumanInputItem, settledLabel: stri
3039
4575
  item.questions.length > 1
3040
4576
  ? `${questionNumberById.get(answer.questionId) ?? index + 1}. `
3041
4577
  : ""
3042
- }${answer.label}: ${answer.values.join(", ") || "Answered"}`,
4578
+ }${answer.label}: ${
4579
+ answer.secret
4580
+ ? `${secretAnswerLabel(answer.secret)} (${answer.secret.reference})`
4581
+ : answer.values.join(", ") || "Answered"
4582
+ }`,
3043
4583
  )
3044
4584
  .join("\n\n");
3045
4585
  return [questions, `${settledLabel}${answers ? `\n\n${answers}` : ""}`]
@@ -3049,9 +4589,11 @@ function humanInputConversationCopyText(item: HumanInputItem, settledLabel: stri
3049
4589
 
3050
4590
  function AgentMessageRow({
3051
4591
  item,
4592
+ renderMessageActions,
3052
4593
  renderMessageText,
3053
4594
  }: {
3054
4595
  item: AgentMessageItem;
4596
+ renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
3055
4597
  renderMessageText?:
3056
4598
  | ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
3057
4599
  | undefined;
@@ -3062,76 +4604,80 @@ function AgentMessageRow({
3062
4604
  const body = renderMessageText ? (
3063
4605
  renderMessageText(item.text, item)
3064
4606
  ) : (
3065
- <Markdown streaming={item.streaming}>{item.text}</Markdown>
4607
+ <Markdown softLineBreaks streaming={item.streaming}>
4608
+ {item.text}
4609
+ </Markdown>
3066
4610
  );
3067
4611
  // While streaming, copy is still useful (current text) but keep chrome calm —
3068
4612
  // stamp only after the message finishes (occurredAt tracks completion).
3069
4613
  return (
3070
- <div data-og-annotation-source-key={item.annotationSource?.eventId}>
3071
- <CopyHoverFrame
3072
- copyText={item.text}
3073
- label="Copy message"
3074
- align="start"
3075
- className={cn(enter && "animate-og-enter", "min-w-0 text-og-md leading-7 text-og-fg")}
3076
- trailing={item.streaming ? null : <MessageFooterTime occurredAt={item.occurredAt} />}
4614
+ <CopyHoverFrame
4615
+ copyText={item.text}
4616
+ label="Copy message"
4617
+ align="start"
4618
+ className={cn(enter && "animate-og-enter", "min-w-0 text-og-md leading-7 text-og-fg")}
4619
+ trailing={
4620
+ item.streaming ? null : (
4621
+ <>
4622
+ {renderMessageActions?.(item)}
4623
+ <MessageFooterTime occurredAt={item.occurredAt} />
4624
+ </>
4625
+ )
4626
+ }
4627
+ >
4628
+ <div
4629
+ data-og-wide-table-message=""
4630
+ data-og-search-item={item.id}
4631
+ data-og-annotation-source-key={item.annotationSource?.eventId}
3077
4632
  >
3078
4633
  {body}
3079
- </CopyHoverFrame>
3080
- </div>
4634
+ </div>
4635
+ </CopyHoverFrame>
3081
4636
  );
3082
4637
  }
3083
4638
 
3084
4639
  /**
3085
- * A worker session reporting back to its manager. The child's completion arrives
3086
- * as a `user.message` carrying a `childCompletion` payload (the raw message text
3087
- * used to render as an "ugly" user bubble); it projects to a `worker-completion`
3088
- * item and draws here as a quietly-confident card — an inbound result, not
3089
- * something the human said. One glyph + one line carry the outcome; the worker's
3090
- * full report, evidence, and any paused reason live behind a collapsed
3091
- * disclosure, and a "View session" affordance deep-links into the child.
3092
- *
3093
- * Color follows the timeline's restraint: green only for a completed goal, the
3094
- * waiting hue only for a paused one, red only for a failed child — everything
3095
- * else is a neutral inbound card.
4640
+ * A worker session reporting back to its manager in older history: the child's
4641
+ * completion arrived as a `user.message` carrying a `childCompletion` payload.
4642
+ * It draws as the same named agent row as every other inbound agent update:
4643
+ * the outcome and goal on the row, the report, evidence, and any paused
4644
+ * reason behind the disclosure, and a deep link into the child.
3096
4645
  */
3097
4646
  type WorkerCompletionMeta = {
3098
- label: string;
4647
+ title: AgentTitleParts;
3099
4648
  icon: ComponentType<{ className?: string }>;
3100
- iconClass: string;
3101
- /** The 2px left-accent hue — color spent only on the exceptional outcomes. */
3102
- accentClass: string;
4649
+ failed: boolean;
3103
4650
  };
3104
4651
 
3105
- function workerCompletionMeta(item: WorkerCompletionItem): WorkerCompletionMeta {
4652
+ function workerCompletionMeta(
4653
+ item: WorkerCompletionItem,
4654
+ name: string | null,
4655
+ ): WorkerCompletionMeta {
3106
4656
  if (item.childStatus === "failed") {
3107
4657
  return {
3108
- label: "Worker failed",
4658
+ title: namedAgentTitle(name, "", " failed", "Worker failed"),
3109
4659
  icon: XCircleIcon,
3110
- iconClass: "text-og-status-failed",
3111
- accentClass: "border-og-status-failed/60",
4660
+ failed: true,
3112
4661
  };
3113
4662
  }
3114
4663
  if (item.goalStatus === "paused") {
3115
4664
  return {
3116
- label: "Worker paused",
4665
+ title: namedAgentTitle(name, "", " paused", "Worker paused"),
3117
4666
  icon: PauseCircleIcon,
3118
- iconClass: "text-og-status-waiting",
3119
- accentClass: "border-og-status-waiting/50",
4667
+ failed: false,
3120
4668
  };
3121
4669
  }
3122
4670
  if (item.goalStatus === "completed") {
3123
4671
  return {
3124
- label: "Worker completed",
4672
+ title: namedAgentTitle(name, "", " completed its goal", "Worker completed"),
3125
4673
  icon: CheckCircle2Icon,
3126
- iconClass: "text-og-status-idle",
3127
- accentClass: "border-og-status-idle/45",
4674
+ failed: false,
3128
4675
  };
3129
4676
  }
3130
4677
  return {
3131
- label: "Worker reported back",
4678
+ title: namedAgentTitle(name, "Result from ", "", "Worker reported back"),
3132
4679
  icon: BotIcon,
3133
- iconClass: "text-og-accent",
3134
- accentClass: "border-og-border-strong",
4680
+ failed: false,
3135
4681
  };
3136
4682
  }
3137
4683
 
@@ -3143,14 +4689,12 @@ function WorkerCompletionRow({
3143
4689
  onOpenSession?: ((sessionId: string) => void) | undefined;
3144
4690
  }) {
3145
4691
  const enter = useEntranceAnimation();
3146
- const [open, setOpen] = useState(false);
3147
- const meta = workerCompletionMeta(item);
4692
+ const identity = useAgentIdentity();
4693
+ const meta = workerCompletionMeta(item, identity.titleFor(item.childSessionId));
3148
4694
  const Icon = meta.icon;
3149
- // The worker's own report is the substance behind the fold; evidence and any
3150
- // paused reason sit alongside it as quieter, labelled context.
3151
4695
  // "Paused because" only when the outcome actually IS a pause — completion
3152
4696
  // payloads can carry a leftover pausedReason/rationale from earlier in the
3153
- // worker's life, and a "Worker completed" card must not show a pause section.
4697
+ // worker's life, and a completed row must not show a pause section.
3154
4698
  const showPausedReason =
3155
4699
  item.childStatus !== "failed" && item.goalStatus === "paused" && !!item.pausedReason?.trim();
3156
4700
  const details: { label: string; value: string; muted?: boolean }[] = [
@@ -3162,82 +4706,44 @@ function WorkerCompletionRow({
3162
4706
  ? [{ label: "Paused because", value: item.pausedReason!.trim(), muted: true }]
3163
4707
  : []),
3164
4708
  ];
3165
- const hasDetails = details.length > 0;
3166
4709
  return (
3167
4710
  <div className={cn(enter && "animate-og-enter", "min-w-0")}>
3168
- {/* An inbound result, not a bubble: a 2px left accent carries the outcome —
3169
- no full frame, no surface fill. The report unfolds flush beneath. */}
3170
- <div className={cn("flex flex-col gap-2 border-l-2 pl-3", meta.accentClass)}>
3171
- <div className="flex items-start gap-2.5">
3172
- <span className={cn("mt-0.5 shrink-0", meta.iconClass)}>
3173
- <Icon className="size-4" />
3174
- </span>
3175
- <div className="min-w-0 flex-1">
3176
- <p className="text-og-base leading-5 text-og-fg">
3177
- <span className="font-medium">{meta.label}</span>
3178
- {item.goalText ? (
3179
- <span className="text-og-fg-muted"> · {truncate(item.goalText, 90)}</span>
3180
- ) : null}
3181
- </p>
3182
- </div>
3183
- {item.childSessionId && onOpenSession ? (
3184
- <button
3185
- type="button"
3186
- onClick={() => onOpenSession(item.childSessionId)}
3187
- className={cn(
3188
- "-my-0.5 -mr-1 inline-flex shrink-0 items-center gap-1 rounded-og-sm px-2 py-1 text-og-sm font-medium text-og-fg-muted pointer-coarse:py-2",
3189
- "outline-hidden transition-colors duration-150 hover:bg-og-surface-2 hover:text-og-fg",
3190
- "focus-visible:ring-2 focus-visible:ring-og-accent",
3191
- )}
3192
- >
3193
- View session
3194
- <ArrowRightIcon className="size-3.5" />
3195
- </button>
3196
- ) : null}
3197
- </div>
3198
- {hasDetails ? (
3199
- <Collapsible.Root open={open} onOpenChange={setOpen}>
3200
- <Collapsible.Trigger asChild>
3201
- <button
3202
- type="button"
3203
- className={cn(
3204
- "group/wc -mx-1 inline-flex w-fit items-center gap-1 rounded-og-sm px-1 py-0.5 text-og-xs font-medium text-og-fg-subtle",
3205
- "outline-hidden transition-colors duration-150 hover:text-og-fg-muted focus-visible:ring-2 focus-visible:ring-og-accent",
3206
- )}
3207
- >
3208
- <ChevronRightIcon className="size-3 transition-transform duration-150 ease-og-in-out group-data-[state=open]/wc:rotate-90" />
3209
- {open ? "Hide details" : "Show details"}
3210
- </button>
3211
- </Collapsible.Trigger>
3212
- <Collapsible.Content className="overflow-hidden data-[state=closed]:animate-og-collapse data-[state=open]:animate-og-expand">
3213
- <div className="ml-1 mt-1.5 flex flex-col gap-2.5">
3214
- {details.map((detail) => (
3215
- <div key={detail.label} className="min-w-0">
3216
- <p className="mb-1 text-og-xs font-medium uppercase tracking-[0.08em] text-og-fg-subtle">
3217
- {detail.label}
3218
- </p>
3219
- <p
3220
- className={cn(
3221
- "whitespace-pre-wrap break-words text-og-sm leading-6",
3222
- detail.muted ? "text-og-fg-subtle" : "text-og-fg-muted",
3223
- )}
3224
- >
3225
- {detail.value}
3226
- </p>
3227
- </div>
3228
- ))}
3229
- </div>
3230
- </Collapsible.Content>
3231
- </Collapsible.Root>
3232
- ) : null}
3233
- </div>
4711
+ <AgentRow
4712
+ icon={<Icon />}
4713
+ title={meta.title}
4714
+ preview={item.goalText ? truncate(item.goalText, 240) : null}
4715
+ previewLines={2}
4716
+ sessionId={item.childSessionId}
4717
+ onOpenSession={onOpenSession ?? identity.onOpenSession}
4718
+ failed={meta.failed}
4719
+ >
4720
+ {details.length > 0 ? (
4721
+ <>
4722
+ {details.map((detail) => (
4723
+ <AgentRowSection key={detail.label} label={detail.label} muted={detail.muted}>
4724
+ {detail.value}
4725
+ </AgentRowSection>
4726
+ ))}
4727
+ </>
4728
+ ) : undefined}
4729
+ </AgentRow>
3234
4730
  </div>
3235
4731
  );
3236
4732
  }
3237
4733
 
3238
- function SessionStatusRow({ item }: { item: { status: SessionStatus; occurredAt: string } }) {
4734
+ function SessionStatusRow({
4735
+ item,
4736
+ }: {
4737
+ item: {
4738
+ status: SessionStatus;
4739
+ blocked?: true;
4740
+ occurredAt: string;
4741
+ resolvedAt?: string;
4742
+ };
4743
+ }) {
3239
4744
  const enter = useEntranceAnimation();
3240
- const meta = SESSION_STATUS_META[item.status];
4745
+ const status = item.blocked ? "blocked" : item.status;
4746
+ const meta = SESSION_STATUS_META[status];
3241
4747
  return (
3242
4748
  <div
3243
4749
  className={cn(
@@ -3248,8 +4754,9 @@ function SessionStatusRow({ item }: { item: { status: SessionStatus; occurredAt:
3248
4754
  >
3249
4755
  <span className="h-px flex-1 bg-og-border" />
3250
4756
  <span className="inline-flex items-center gap-1.5">
3251
- <StatusDot status={item.status} className="size-1" />
3252
- {meta.label.toLowerCase()} · {formatRelativeTime(item.occurredAt)}
4757
+ {item.resolvedAt ? null : <StatusDot status={status} className="size-1" />}
4758
+ {item.resolvedAt ? "work resumed" : meta.label.toLowerCase()} ·{" "}
4759
+ {formatRelativeTime(item.resolvedAt ?? item.occurredAt)}
3253
4760
  </span>
3254
4761
  <span className="h-px flex-1 bg-og-border" />
3255
4762
  </div>
@@ -3277,41 +4784,46 @@ type GoalMeta = { label: string; pill: string; icon: ComponentType<{ className?:
3277
4784
 
3278
4785
  const NEUTRAL_PILL = "border-og-border bg-og-surface-1 text-og-fg-muted";
3279
4786
 
3280
- const GOAL_META: Record<GoalItem["action"], GoalMeta> = {
3281
- set: {
3282
- label: "Goal set",
3283
- pill: "border-og-accent/30 bg-og-accent/10 text-og-accent",
3284
- icon: TargetIcon,
3285
- },
3286
- updated: { label: "Goal updated", pill: NEUTRAL_PILL, icon: PencilLineIcon },
3287
- completed: {
3288
- label: "Goal completed",
3289
- pill: "border-og-status-idle/30 bg-og-status-idle/10 text-og-status-idle",
3290
- icon: CheckIcon,
3291
- },
3292
- paused: {
3293
- label: "Goal paused",
3294
- pill: WAITING_PILL_CLASS,
3295
- icon: PauseIcon,
3296
- },
3297
- resumed: { label: "Goal resumed", pill: NEUTRAL_PILL, icon: PlayIcon },
3298
- cleared: { label: "Goal cleared", pill: NEUTRAL_PILL, icon: Trash2Icon },
3299
- held: {
3300
- label: "Goal held",
3301
- pill: WAITING_PILL_CLASS,
3302
- icon: PauseCircleIcon,
3303
- },
3304
- continuation: { label: "Continuing toward the goal", pill: NEUTRAL_PILL, icon: ArrowRightIcon },
3305
- };
4787
+ // Shared production chunks can be cyclic. Resolve icon bindings during render,
4788
+ // after their modules initialize, rather than permanently capturing undefined.
4789
+ function goalMeta(action: GoalItem["action"]): GoalMeta {
4790
+ const metadata: Record<GoalItem["action"], GoalMeta> = {
4791
+ set: {
4792
+ label: "Goal set",
4793
+ pill: "border-og-accent/30 bg-og-accent/10 text-og-accent",
4794
+ icon: TargetIcon,
4795
+ },
4796
+ updated: { label: "Goal updated", pill: NEUTRAL_PILL, icon: PencilLineIcon },
4797
+ completed: {
4798
+ label: "Goal completed",
4799
+ pill: "border-og-status-idle/30 bg-og-status-idle/10 text-og-status-idle",
4800
+ icon: CheckIcon,
4801
+ },
4802
+ paused: {
4803
+ label: "Goal paused",
4804
+ pill: WAITING_PILL_CLASS,
4805
+ icon: PauseIcon,
4806
+ },
4807
+ resumed: { label: "Goal resumed", pill: NEUTRAL_PILL, icon: PlayIcon },
4808
+ cleared: { label: "Goal cleared", pill: NEUTRAL_PILL, icon: Trash2Icon },
4809
+ held: {
4810
+ label: "Goal held",
4811
+ pill: WAITING_PILL_CLASS,
4812
+ icon: PauseCircleIcon,
4813
+ },
4814
+ continuation: { label: "Continuing toward the goal", pill: NEUTRAL_PILL, icon: ArrowRightIcon },
4815
+ };
4816
+ return metadata[action];
4817
+ }
3306
4818
 
3307
4819
  /**
3308
4820
  * A goal landmark pill. Resolves its label, accent/tone, and glyph from
3309
- * {@link GOAL_META} so all six actions are visually distinguishable while the
4821
+ * {@link goalMeta} so all six actions are visually distinguishable while the
3310
4822
  * palette stays restrained — see that table for the per-action rationale.
3311
4823
  */
3312
4824
  function GoalRow({ item }: { item: GoalItem }) {
3313
4825
  const enter = useEntranceAnimation();
3314
- const { label, pill, icon: Icon } = GOAL_META[item.action];
4826
+ const { label, pill, icon: Icon } = goalMeta(item.action);
3315
4827
  return (
3316
4828
  <div className={cn(enter && "animate-og-enter", "flex justify-center")}>
3317
4829
  <span
@@ -3332,13 +4844,16 @@ function GoalRow({ item }: { item: GoalItem }) {
3332
4844
 
3333
4845
  function MachineInputBatchRow({
3334
4846
  item,
4847
+ onOpenSession,
3335
4848
  loadVideoArtifactPlayback,
3336
4849
  }: {
3337
4850
  item: MachineInputBatchItem;
4851
+ onOpenSession?: ((sessionId: string) => void) | undefined;
3338
4852
  loadVideoArtifactPlayback?: VideoArtifactPlaybackLoader | undefined;
3339
4853
  }) {
3340
4854
  const enter = useEntranceAnimation();
3341
- const label = machineInputBatchLabel(item.members);
4855
+ const identity = useAgentIdentity();
4856
+ const label = machineInputBatchLabel(item.members, identity.titleFor);
3342
4857
  const single = item.members.length === 1 ? item.members[0]! : null;
3343
4858
  if (single?.kind === "media_generation_result" && single.result) {
3344
4859
  return (
@@ -3349,12 +4864,21 @@ function MachineInputBatchRow({
3349
4864
  );
3350
4865
  }
3351
4866
  const singleSummary = single ? cleanMachineInputSummary(single.summary) : "";
4867
+ // An agent's words live in its named row behind the pill; the pill already
4868
+ // says who it is from, so nothing repeats beneath it.
3352
4869
  const showCollapsedSummary =
3353
- single != null && machineInputSummaryIsUseful(single.kind, singleSummary);
4870
+ !item.compact &&
4871
+ single != null &&
4872
+ !isAgentMember(single) &&
4873
+ machineInputSummaryIsUseful(single.kind, singleSummary);
3354
4874
 
3355
4875
  return (
3356
4876
  <div className={cn(enter && "animate-og-enter", "flex flex-col items-center gap-1.5")}>
3357
- <details className="group w-full max-w-full" data-og-machine-input-batch="">
4877
+ <details
4878
+ className="group w-full max-w-full"
4879
+ data-og-machine-input-batch=""
4880
+ title={`Received ${formatClockTime(item.occurredAt)}`}
4881
+ >
3358
4882
  <summary className="flex cursor-pointer list-none justify-center [&::-webkit-details-marker]:hidden">
3359
4883
  <span
3360
4884
  className={cn(
@@ -3369,14 +4893,26 @@ function MachineInputBatchRow({
3369
4893
  <span className="truncate">{label}</span>
3370
4894
  </span>
3371
4895
  </summary>
3372
- <div className="mx-auto mt-2 w-full max-w-lg space-y-2 border-t border-og-border/50 pt-2">
3373
- {item.members.map((member) => (
3374
- <MachineInputRow
3375
- key={member.id}
3376
- member={member}
3377
- loadVideoArtifactPlayback={loadVideoArtifactPlayback}
3378
- />
3379
- ))}
4896
+ <div className="mx-auto mt-2 w-full max-w-lg space-y-1.5 border-t border-og-border/50 pt-2">
4897
+ <p className="text-og-xs text-og-fg-subtle">
4898
+ Received <time dateTime={item.occurredAt}>{formatClockTime(item.occurredAt)}</time>
4899
+ </p>
4900
+ {groupMachineInputMembers(item.members).map((group) =>
4901
+ group.kind === "agent" ? (
4902
+ <AgentMemberRow
4903
+ key={group.members[0]!.id}
4904
+ members={group.members}
4905
+ onOpenSession={onOpenSession}
4906
+ />
4907
+ ) : (
4908
+ <MachineInputRow
4909
+ key={group.member.id}
4910
+ member={group.member}
4911
+ onOpenSession={onOpenSession}
4912
+ loadVideoArtifactPlayback={loadVideoArtifactPlayback}
4913
+ />
4914
+ ),
4915
+ )}
3380
4916
  </div>
3381
4917
  </details>
3382
4918
  {showCollapsedSummary ? (
@@ -3388,11 +4924,94 @@ function MachineInputBatchRow({
3388
4924
  );
3389
4925
  }
3390
4926
 
4927
+ type MachineInputMemberGroup =
4928
+ | { kind: "agent"; members: MachineInputBatchItem["members"] }
4929
+ | { kind: "other"; member: MachineInputBatchItem["members"][number] };
4930
+
4931
+ /**
4932
+ * Agent members get named rows; repeated progress notes from one agent fold
4933
+ * into its single row (latest note shown, earlier ones behind the disclosure).
4934
+ */
4935
+ function groupMachineInputMembers(
4936
+ members: MachineInputBatchItem["members"],
4937
+ ): MachineInputMemberGroup[] {
4938
+ const groups: MachineInputMemberGroup[] = [];
4939
+ const progressBySource = new Map<string, MachineInputBatchItem["members"]>();
4940
+ for (const member of members) {
4941
+ if (!isAgentMember(member)) {
4942
+ groups.push({ kind: "other", member });
4943
+ continue;
4944
+ }
4945
+ if (member.kind === "child_progress") {
4946
+ const existing = progressBySource.get(member.sourceId);
4947
+ if (existing) {
4948
+ existing.push(member);
4949
+ continue;
4950
+ }
4951
+ const bucket = [member];
4952
+ progressBySource.set(member.sourceId, bucket);
4953
+ groups.push({ kind: "agent", members: bucket });
4954
+ continue;
4955
+ }
4956
+ groups.push({ kind: "agent", members: [member] });
4957
+ }
4958
+ return groups;
4959
+ }
4960
+
4961
+ function agentMemberIcon(member: MachineInputBatchItem["members"][number]): ReactNode {
4962
+ if (member.kind === "child_terminal_result" && member.classification === "failure") {
4963
+ return <XCircleIcon />;
4964
+ }
4965
+ if (member.kind === "child_paused") return <PauseCircleIcon />;
4966
+ return <CornerDownRightIcon />;
4967
+ }
4968
+
4969
+ /** One named row for an update sent by (or about) another agent. */
4970
+ function AgentMemberRow({
4971
+ members,
4972
+ onOpenSession,
4973
+ }: {
4974
+ members: MachineInputBatchItem["members"];
4975
+ onOpenSession?: ((sessionId: string) => void) | undefined;
4976
+ }) {
4977
+ const identity = useAgentIdentity();
4978
+ const latest = members[members.length - 1]!;
4979
+ const sessionId = agentMemberSessionId(latest);
4980
+ const title = agentMemberTitle(latest, identity.titleFor(sessionId));
4981
+ const texts = members.map((member) => agentMemberText(member));
4982
+ const latestText = texts[texts.length - 1]!;
4983
+ const bodies = texts
4984
+ .map((text, index) => ({ id: members[index]!.id, body: text.body }))
4985
+ .filter((entry) => entry.body.length > 0);
4986
+ return (
4987
+ <AgentRow
4988
+ icon={agentMemberIcon(latest)}
4989
+ title={title}
4990
+ preview={latestText.preview ? truncate(latestText.preview, 320) : null}
4991
+ previewLines={2}
4992
+ meta={members.length > 1 ? `+${members.length - 1} earlier` : null}
4993
+ sessionId={sessionId}
4994
+ onOpenSession={onOpenSession ?? identity.onOpenSession}
4995
+ failed={latest.kind === "child_terminal_result" && latest.classification === "failure"}
4996
+ >
4997
+ {bodies.length > 0 ? (
4998
+ <>
4999
+ {bodies.map((entry) => (
5000
+ <AgentRowText key={entry.id}>{entry.body}</AgentRowText>
5001
+ ))}
5002
+ </>
5003
+ ) : undefined}
5004
+ </AgentRow>
5005
+ );
5006
+ }
5007
+
3391
5008
  function MachineInputRow({
3392
5009
  member,
5010
+ onOpenSession,
3393
5011
  loadVideoArtifactPlayback,
3394
5012
  }: {
3395
5013
  member: MachineInputBatchItem["members"][number];
5014
+ onOpenSession?: ((sessionId: string) => void) | undefined;
3396
5015
  loadVideoArtifactPlayback?: VideoArtifactPlaybackLoader | undefined;
3397
5016
  }) {
3398
5017
  if (member.kind === "media_generation_result" && member.result) {
@@ -3419,6 +5038,11 @@ function MachineInputRow({
3419
5038
  {truncate(summary, 320)}
3420
5039
  </p>
3421
5040
  ) : null}
5041
+ <ChildSessionLink
5042
+ kind={member.kind}
5043
+ sourceId={member.sourceId}
5044
+ onOpenSession={onOpenSession}
5045
+ />
3422
5046
  </div>
3423
5047
  </div>
3424
5048
  );
@@ -3492,12 +5116,36 @@ function formatVideoDuration(seconds: number): string {
3492
5116
 
3493
5117
  function NoticeRow({ item }: { item: NoticeItem }) {
3494
5118
  const enter = useEntranceAnimation();
5119
+ if (item.allowance) return <AllowanceExhaustedRow refusal={item.allowance} />;
5120
+ if (item.sessionUsageLimit) return <SessionUsageLimitRow stop={item.sessionUsageLimit} />;
5121
+ if (item.recordedOutcome) {
5122
+ return (
5123
+ <details
5124
+ className="group text-og-sm text-og-fg-muted"
5125
+ role="note"
5126
+ data-og-recorded-outcome="wait"
5127
+ >
5128
+ <summary className="flex cursor-pointer list-none items-center gap-2 py-1 [&::-webkit-details-marker]:hidden">
5129
+ <ChevronRightIcon
5130
+ aria-hidden
5131
+ className="size-3.5 transition-transform group-open:rotate-90"
5132
+ />
5133
+ <span title={new Date(item.occurredAt).toLocaleString()}>
5134
+ {recordedWaitSummary(item)}
5135
+ </span>
5136
+ </summary>
5137
+ <p className="mt-1 whitespace-pre-wrap break-words pl-5 text-og-fg-muted">{item.text}</p>
5138
+ </details>
5139
+ );
5140
+ }
5141
+ const presentedTone = noticeTone(item);
5142
+ const resolvedApproval = noticeIsResolvedApproval(item);
3495
5143
  const tone =
3496
- item.tone === "failed"
5144
+ presentedTone === "failed"
3497
5145
  ? "border-og-status-failed/35 bg-og-status-failed/10 text-og-status-failed"
3498
- : item.tone === "waiting"
5146
+ : presentedTone === "waiting"
3499
5147
  ? WAITING_PILL_CLASS
3500
- : "border-og-border bg-og-surface-1 text-og-fg-muted";
5148
+ : NEUTRAL_PILL;
3501
5149
  return (
3502
5150
  <div
3503
5151
  className={cn(
@@ -3507,16 +5155,22 @@ function NoticeRow({ item }: { item: NoticeItem }) {
3507
5155
  )}
3508
5156
  role="status"
3509
5157
  >
3510
- <TriangleAlertIcon
3511
- className={cn("mt-0.5 size-4 shrink-0", item.tone === "cancelled" && "opacity-60")}
3512
- />
5158
+ {resolvedApproval ? (
5159
+ <CheckIcon className="mt-0.5 size-4 shrink-0 opacity-70" aria-hidden="true" />
5160
+ ) : (
5161
+ <TriangleAlertIcon
5162
+ className={cn("mt-0.5 size-4 shrink-0", item.tone === "cancelled" && "opacity-60")}
5163
+ />
5164
+ )}
3513
5165
  <div className="min-w-0 flex-1">
3514
- <span className="whitespace-pre-wrap break-words">{item.text}</span>
5166
+ <span className="whitespace-pre-wrap break-words">{noticeDisplayText(item)}</span>
3515
5167
  {item.details ? (
3516
5168
  <details className="mt-2 text-og-control">
3517
5169
  <summary className="cursor-pointer font-medium">{item.details.label}</summary>
3518
- <pre className="mt-2 max-h-64 overflow-auto whitespace-pre-wrap break-all rounded-og-sm bg-black/5 p-2 font-mono dark:bg-white/5">
3519
- {JSON.stringify(item.details.value, null, 2)}
5170
+ <pre className="mt-2 max-h-64 overflow-auto whitespace-pre-wrap break-all rounded-og-sm bg-og-fg/5 p-2 font-mono">
5171
+ {typeof item.details.value === "string"
5172
+ ? item.details.value
5173
+ : JSON.stringify(item.details.value, null, 2)}
3520
5174
  </pre>
3521
5175
  </details>
3522
5176
  ) : null}
@@ -3535,6 +5189,41 @@ function NoticeRow({ item }: { item: NoticeItem }) {
3535
5189
  );
3536
5190
  }
3537
5191
 
5192
+ /**
5193
+ * "Waited for 2 agents · 3m 5s" once later input ended the wait, otherwise
5194
+ * "Waiting for 2 agents · since 10:32" (with the date on a later day). The
5195
+ * reason stays behind the disclosure.
5196
+ */
5197
+ function recordedWaitSummary(item: NoticeItem): ReactNode {
5198
+ const agents = item.waitingAgents ?? 0;
5199
+ const target = agents > 0 ? ` for ${agents} ${agents === 1 ? "agent" : "agents"}` : "";
5200
+ const waitedMs = item.waitEndedAt
5201
+ ? durationBetween(item.occurredAt, item.waitEndedAt)
5202
+ : undefined;
5203
+ if (waitedMs !== undefined) {
5204
+ return (
5205
+ <>
5206
+ Waited{target} ·{" "}
5207
+ <time dateTime={`PT${Math.floor(waitedMs / 1000)}S`}>{formatElapsed(waitedMs)}</time>
5208
+ </>
5209
+ );
5210
+ }
5211
+ // An open wait is historical once its day has passed; keep its date visible.
5212
+ const started = new Date(item.occurredAt);
5213
+ const sameDay = started.toDateString() === new Date().toDateString();
5214
+ return (
5215
+ <>
5216
+ {item.waitEndedAt ? "Waited" : "Waiting"}
5217
+ {target} · since{" "}
5218
+ <time dateTime={item.occurredAt}>
5219
+ {sameDay
5220
+ ? formatClockTime(item.occurredAt)
5221
+ : started.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" })}
5222
+ </time>
5223
+ </>
5224
+ );
5225
+ }
5226
+
3538
5227
  /**
3539
5228
  * The inline connection-recovery card: missing or lapsed access surfaces as a
3540
5229
  * calm, tappable affordance instead of a raw provider-domain error. The `reason`
@@ -3581,6 +5270,7 @@ function AuthNeededRow({
3581
5270
  ? `${provider} tools unavailable`
3582
5271
  : `${actionLabel} ${provider}`;
3583
5272
  const reasonLine = recommendation?.rationale ?? authReasonLine(item.reason);
5273
+ const hostAuthorizationUrl = item.authoritySource === "host" ? item.authorizationUrl : null;
3584
5274
 
3585
5275
  const start = async () => {
3586
5276
  if (!onReconnect || busy) {
@@ -3623,27 +5313,40 @@ function AuthNeededRow({
3623
5313
  ) : null}
3624
5314
  </div>
3625
5315
  </div>
3626
- {!unavailable && onReconnect ? (
5316
+ {hostAuthorizationUrl ? (
5317
+ <a
5318
+ href={hostAuthorizationUrl}
5319
+ rel="noreferrer"
5320
+ target="_blank"
5321
+ className={cn(
5322
+ PRIMARY_ACTION_CLASS,
5323
+ "transition-colors hover:bg-og-primary-hover pointer-coarse:min-h-9",
5324
+ )}
5325
+ >
5326
+ <RefreshCwIcon className="size-3.5" aria-hidden />
5327
+ {actionLabel}
5328
+ </a>
5329
+ ) : item.authoritySource !== "host" && !unavailable && onReconnect ? (
3627
5330
  <button
3628
5331
  type="button"
3629
5332
  onClick={() => void start()}
3630
5333
  disabled={busy}
3631
5334
  className={cn(
3632
5335
  PRIMARY_ACTION_CLASS,
3633
- "transition-colors hover:bg-og-accent-strong disabled:opacity-70 pointer-coarse:min-h-9",
5336
+ "transition-colors hover:bg-og-primary-hover disabled:opacity-50 pointer-coarse:min-h-9",
3634
5337
  )}
3635
5338
  >
3636
5339
  <RefreshCwIcon className={cn("size-3.5", busy && "animate-og-spin")} aria-hidden />
3637
5340
  {busy ? "Opening…" : actionLabel}
3638
5341
  </button>
3639
- ) : !unavailable && item.authorizationUrl ? (
5342
+ ) : item.authoritySource !== "host" && !unavailable && item.authorizationUrl ? (
3640
5343
  <a
3641
5344
  href={item.authorizationUrl}
3642
5345
  rel="noreferrer"
3643
5346
  target="_blank"
3644
5347
  className={cn(
3645
5348
  PRIMARY_ACTION_CLASS,
3646
- "transition-colors hover:bg-og-accent-strong pointer-coarse:min-h-9",
5349
+ "transition-colors hover:bg-og-primary-hover pointer-coarse:min-h-9",
3647
5350
  )}
3648
5351
  >
3649
5352
  <RefreshCwIcon className="size-3.5" aria-hidden />