@opengeni/react 3.1.0 → 3.1.2

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 (863) hide show
  1. package/README.md +769 -22
  2. package/dist/accounts.js +89 -11
  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-BO2IWXVK.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-RGALCTWO.js → chunk-2I6R3GCZ.js} +4285 -3980
  21. package/dist/chunk-2I6R3GCZ.js.map +1 -0
  22. package/dist/{chunk-D3UIAJKY.js → chunk-2SFFYIH3.js} +1179 -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-3UZCWK46.js +298 -0
  27. package/dist/chunk-3UZCWK46.js.map +1 -0
  28. package/dist/chunk-47CKUXTW.js +293 -0
  29. package/dist/chunk-47CKUXTW.js.map +1 -0
  30. package/dist/chunk-4QYP6QUI.js +109 -0
  31. package/dist/chunk-4QYP6QUI.js.map +1 -0
  32. package/dist/chunk-5EY26VZ7.js +1053 -0
  33. package/dist/chunk-5EY26VZ7.js.map +1 -0
  34. package/dist/chunk-5I4VRWBB.js +56 -0
  35. package/dist/chunk-5I4VRWBB.js.map +1 -0
  36. package/dist/chunk-5WOR7UCL.js +191 -0
  37. package/dist/chunk-5WOR7UCL.js.map +1 -0
  38. package/dist/chunk-5YJLGOM5.js +51 -0
  39. package/dist/chunk-5YJLGOM5.js.map +1 -0
  40. package/dist/chunk-6JFN3OWG.js +386 -0
  41. package/dist/chunk-6JFN3OWG.js.map +1 -0
  42. package/dist/chunk-7QJT5O4S.js +35 -0
  43. package/dist/chunk-7QJT5O4S.js.map +1 -0
  44. package/dist/chunk-A55KV72M.js +402 -0
  45. package/dist/chunk-A55KV72M.js.map +1 -0
  46. package/dist/chunk-ADWCLI4G.js +2028 -0
  47. package/dist/chunk-ADWCLI4G.js.map +1 -0
  48. package/dist/chunk-B27E3V37.js +29 -0
  49. package/dist/chunk-B27E3V37.js.map +1 -0
  50. package/dist/chunk-B7TC2A33.js +83 -0
  51. package/dist/chunk-B7TC2A33.js.map +1 -0
  52. package/dist/chunk-BLL56F7R.js +385 -0
  53. package/dist/chunk-BLL56F7R.js.map +1 -0
  54. package/dist/chunk-BOFEQC6K.js +162 -0
  55. package/dist/chunk-BOFEQC6K.js.map +1 -0
  56. package/dist/chunk-CM7LVUKC.js +35 -0
  57. package/dist/chunk-CM7LVUKC.js.map +1 -0
  58. package/dist/chunk-CORUP6JB.js +122 -0
  59. package/dist/chunk-CORUP6JB.js.map +1 -0
  60. package/dist/chunk-CSX5FFKZ.js +35 -0
  61. package/dist/chunk-CSX5FFKZ.js.map +1 -0
  62. package/dist/chunk-EGVHJKGJ.js +3125 -0
  63. package/dist/chunk-EGVHJKGJ.js.map +1 -0
  64. package/dist/{chunk-NEXLLTP4.js → chunk-G76LSA76.js} +6 -5
  65. package/dist/chunk-G76LSA76.js.map +1 -0
  66. package/dist/{chunk-PEI6RANI.js → chunk-GQMRL52F.js} +311 -75
  67. package/dist/chunk-GQMRL52F.js.map +1 -0
  68. package/dist/{chunk-ELIYWBZR.js → chunk-GUUXOISA.js} +1113 -457
  69. package/dist/chunk-GUUXOISA.js.map +1 -0
  70. package/dist/{chunk-HUIKIDM5.js → chunk-I7X2P4FU.js} +225 -116
  71. package/dist/chunk-I7X2P4FU.js.map +1 -0
  72. package/dist/chunk-IGFZFLI2.js +1390 -0
  73. package/dist/chunk-IGFZFLI2.js.map +1 -0
  74. package/dist/{chunk-4MDLEHXL.js → chunk-IMSUVYQB.js} +26 -13
  75. package/dist/chunk-IMSUVYQB.js.map +1 -0
  76. package/dist/chunk-IOYHDEP3.js +38 -0
  77. package/dist/chunk-IOYHDEP3.js.map +1 -0
  78. package/dist/chunk-IZV5FTJC.js +55 -0
  79. package/dist/chunk-IZV5FTJC.js.map +1 -0
  80. package/dist/chunk-KF33GTWN.js +343 -0
  81. package/dist/chunk-KF33GTWN.js.map +1 -0
  82. package/dist/chunk-KSGQ5ODA.js +457 -0
  83. package/dist/chunk-KSGQ5ODA.js.map +1 -0
  84. package/dist/{chunk-OAM4WAAM.js → chunk-KZC6QRNR.js} +1513 -697
  85. package/dist/chunk-KZC6QRNR.js.map +1 -0
  86. package/dist/chunk-LJUMYUBO.js +218 -0
  87. package/dist/chunk-LJUMYUBO.js.map +1 -0
  88. package/dist/chunk-LSW2OJVV.js +380 -0
  89. package/dist/chunk-LSW2OJVV.js.map +1 -0
  90. package/dist/chunk-M357MHH3.js +2 -0
  91. package/dist/chunk-M357MHH3.js.map +1 -0
  92. package/dist/chunk-MK2TQO4Y.js +24 -0
  93. package/dist/chunk-MK2TQO4Y.js.map +1 -0
  94. package/dist/chunk-N7S72EXK.js +25 -0
  95. package/dist/chunk-N7S72EXK.js.map +1 -0
  96. package/dist/{chunk-XXWH2JLC.js → chunk-NKMYZFHA.js} +616 -621
  97. package/dist/chunk-NKMYZFHA.js.map +1 -0
  98. package/dist/chunk-NX4ADH73.js +14 -0
  99. package/dist/chunk-NX4ADH73.js.map +1 -0
  100. package/dist/chunk-O7F6UJO4.js +71 -0
  101. package/dist/chunk-O7F6UJO4.js.map +1 -0
  102. package/dist/chunk-OVB5WIG5.js +14064 -0
  103. package/dist/chunk-OVB5WIG5.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-MZ2G2XBA.js → chunk-V5ZUJT2T.js} +205 -45
  135. package/dist/chunk-V5ZUJT2T.js.map +1 -0
  136. package/dist/chunk-VIUEOHXY.js +394 -0
  137. package/dist/chunk-VIUEOHXY.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-YJFGAFBD.js → chunk-X5UVUIMF.js} +1892 -1223
  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/fleet-tile.d.ts +2 -2
  200. package/dist/components/grok-mark.d.ts +26 -0
  201. package/dist/components/human-input-form.d.ts +28 -32
  202. package/dist/components/human-input-surface.d.ts +7 -2
  203. package/dist/components/linkified-text.d.ts +18 -0
  204. package/dist/components/machine-input-display.d.ts +22 -1
  205. package/dist/components/machines/agent-status-icon.d.ts +6 -0
  206. package/dist/components/machines/machine-detail.d.ts +7 -1
  207. package/dist/components/machines/machine-status.d.ts +48 -0
  208. package/dist/components/markdown-table-layout.d.ts +18 -0
  209. package/dist/components/markdown.d.ts +37 -4
  210. package/dist/components/message-timeline.d.ts +86 -7
  211. package/dist/components/model-mark.d.ts +29 -0
  212. package/dist/components/model-policy-picker-menu.d.ts +9 -0
  213. package/dist/components/model-policy-picker.d.ts +41 -25
  214. package/dist/components/new-conversation.d.ts +32 -0
  215. package/dist/components/open-geni-chat.d.ts +73 -0
  216. package/dist/components/open-geni-links.d.ts +68 -0
  217. package/dist/components/openrouter-mark.d.ts +6 -0
  218. package/dist/components/opper-mark.d.ts +7 -0
  219. package/dist/components/preview-loading.d.ts +2 -0
  220. package/dist/components/provider-recovery-notice.d.ts +16 -0
  221. package/dist/components/queue-item-content.d.ts +9 -0
  222. package/dist/components/rehype-secret-refs.d.ts +23 -0
  223. package/dist/components/remark-soft-line-breaks.d.ts +38 -0
  224. package/dist/components/sandbox-files.d.ts +3 -1
  225. package/dist/components/sandbox-workspace.d.ts +18 -0
  226. package/dist/components/secret-question-field.d.ts +31 -0
  227. package/dist/components/secret-ref-chip.d.ts +35 -0
  228. package/dist/components/session-capability-frame.d.ts +37 -0
  229. package/dist/components/session-chrome.d.ts +57 -8
  230. package/dist/components/session-commands-panel.d.ts +6 -0
  231. package/dist/components/session-commands.d.ts +6 -0
  232. package/dist/components/session-connection-request.d.ts +8 -0
  233. package/dist/components/session-conversation.d.ts +157 -0
  234. package/dist/components/session-list.d.ts +39 -0
  235. package/dist/components/session-mcp-capability-card.d.ts +41 -0
  236. package/dist/components/session-proxy-scope.d.ts +35 -0
  237. package/dist/components/session-status.d.ts +4 -1
  238. package/dist/components/session-usage-limit-notice.d.ts +39 -0
  239. package/dist/components/timeline-anchor.d.ts +24 -0
  240. package/dist/components/timeline-annotation-chrome.d.ts +25 -0
  241. package/dist/components/timeline-annotation-layout.d.ts +42 -0
  242. package/dist/components/timeline-annotation-markers.d.ts +8 -0
  243. package/dist/components/timeline-annotation-reveal-context.d.ts +4 -0
  244. package/dist/components/timeline-annotation-shared.d.ts +31 -0
  245. package/dist/components/timeline-annotations-dialog.d.ts +3 -2
  246. package/dist/components/timeline-annotations.d.ts +13 -3
  247. package/dist/components/timeline-reading-progress.d.ts +11 -0
  248. package/dist/components/timeline-search.d.ts +34 -0
  249. package/dist/components/timeline-settlement.d.ts +16 -0
  250. package/dist/components/tip-follow.d.ts +16 -0
  251. package/dist/components/tool-action-review.d.ts +47 -0
  252. package/dist/components/tool-review-history.d.ts +25 -0
  253. package/dist/components/usage-limit-notice.d.ts +49 -0
  254. package/dist/components/usage-member-list.d.ts +91 -0
  255. package/dist/components/usage-meter.d.ts +73 -0
  256. package/dist/components/use-viewer-menu-dismiss.d.ts +2 -0
  257. package/dist/components/user-message-body.d.ts +10 -1
  258. package/dist/composer-messages.d.ts +74 -0
  259. package/dist/composer-messages.js +9 -0
  260. package/dist/composer-messages.js.map +1 -0
  261. package/dist/composer-voice-events.d.ts +8 -0
  262. package/dist/composer.d.ts +5 -4
  263. package/dist/composer.js +26 -13
  264. package/dist/computer-viewer-GMLDXPON.js +20 -0
  265. package/dist/computer-viewer-GMLDXPON.js.map +1 -0
  266. package/dist/connect-account-identity.d.ts +8 -0
  267. package/dist/connect-accounts.d.ts +15 -0
  268. package/dist/connect-chooser.d.ts +18 -0
  269. package/dist/connect-panel.d.ts +18 -0
  270. package/dist/connect-setup.d.ts +18 -0
  271. package/dist/connect.d.ts +21 -0
  272. package/dist/connect.js +2149 -0
  273. package/dist/connect.js.map +1 -0
  274. package/dist/connection-catalog.d.ts +38 -0
  275. package/dist/connection-discovery.d.ts +12 -0
  276. package/dist/connection-installed.d.ts +17 -0
  277. package/dist/connection-logo.d.ts +9 -0
  278. package/dist/connection-service-logo.d.ts +8 -0
  279. package/dist/connection-service-presentation.d.ts +13 -0
  280. package/dist/connection-skeleton.d.ts +5 -0
  281. package/dist/connection-type-picker.d.ts +6 -0
  282. package/dist/conversation-timeline.d.ts +5 -0
  283. package/dist/desktop.d.ts +7 -0
  284. package/dist/desktop.js +27 -0
  285. package/dist/desktop.js.map +1 -0
  286. package/dist/device-authorization.d.ts +23 -0
  287. package/dist/diffs.d.ts +8 -0
  288. package/dist/diffs.js +15 -0
  289. package/dist/diffs.js.map +1 -0
  290. package/dist/discovery-cache.d.ts +5 -0
  291. package/dist/editor.d.ts +7 -0
  292. package/dist/editor.js +22 -0
  293. package/dist/editor.js.map +1 -0
  294. package/dist/embedded-new-voice-OCRNT6U5.js +18 -0
  295. package/dist/embedded-new-voice-OCRNT6U5.js.map +1 -0
  296. package/dist/embedded-voice-RPHPYVBT.js +18 -0
  297. package/dist/embedded-voice-RPHPYVBT.js.map +1 -0
  298. package/dist/{fleet-decision-row-GGCAT4E4.js → fleet-decision-row-SI63O5V4.js} +4 -1
  299. package/dist/fleet-decision-row-SI63O5V4.js.map +1 -0
  300. package/dist/hooks/latest-question.d.ts +25 -0
  301. package/dist/hooks/shared-machines-list.d.ts +4 -0
  302. package/dist/hooks/shared-query.d.ts +113 -0
  303. package/dist/hooks/use-attached-browsers.d.ts +1 -1
  304. package/dist/hooks/use-auth-runs.d.ts +1 -4
  305. package/dist/hooks/use-available-models.d.ts +1 -1
  306. package/dist/hooks/use-browser-frame-stream.d.ts +1 -1
  307. package/dist/hooks/use-browser-session.d.ts +12 -1
  308. package/dist/hooks/use-client-config-flags.d.ts +49 -0
  309. package/dist/hooks/use-codex-accounts.d.ts +59 -10
  310. package/dist/hooks/use-composer.d.ts +24 -2
  311. package/dist/hooks/use-computer-frame-stream.d.ts +1 -1
  312. package/dist/hooks/use-computer-input-posture.d.ts +13 -0
  313. package/dist/hooks/use-computer-session.d.ts +4 -0
  314. package/dist/hooks/use-connect.d.ts +23 -0
  315. package/dist/hooks/use-desktop-stream.d.ts +1 -1
  316. package/dist/hooks/use-file-attachments.d.ts +12 -0
  317. package/dist/hooks/use-goal.d.ts +2 -1
  318. package/dist/hooks/use-last-started-turn-policy.d.ts +5 -1
  319. package/dist/hooks/use-new-conversation.d.ts +56 -0
  320. package/dist/hooks/use-new-session-draft.d.ts +74 -0
  321. package/dist/hooks/use-realtime-voice-models.d.ts +7 -0
  322. package/dist/hooks/use-sandbox-files.d.ts +4 -1
  323. package/dist/hooks/use-session-conversation.d.ts +49 -0
  324. package/dist/hooks/use-session-events.d.ts +28 -7
  325. package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
  326. package/dist/hooks/use-session-model-account.d.ts +48 -0
  327. package/dist/hooks/use-supergrok-accounts.d.ts +7 -1
  328. package/dist/hooks/use-terminal-stream.d.ts +11 -5
  329. package/dist/hooks/use-usage.d.ts +40 -0
  330. package/dist/hooks/use-vaults.d.ts +53 -0
  331. package/dist/hooks/use-voice-input.d.ts +8 -1
  332. package/dist/hooks/use-workspace-edit.d.ts +4 -0
  333. package/dist/hooks/use-workspace-sessions.d.ts +22 -5
  334. package/dist/human-input-model.d.ts +122 -0
  335. package/dist/identity-link-accounts.d.ts +11 -0
  336. package/dist/identity-link-consent.d.ts +18 -0
  337. package/dist/index.d.ts +82 -20
  338. package/dist/index.js +2350 -6605
  339. package/dist/index.js.map +1 -1
  340. package/dist/interaction.d.ts +1 -1
  341. package/dist/interaction.js +14 -24
  342. package/dist/interaction.js.map +1 -1
  343. package/dist/latest-question-R77KWSUP.js +118 -0
  344. package/dist/latest-question-R77KWSUP.js.map +1 -0
  345. package/dist/lib/clipboard.d.ts +7 -0
  346. package/dist/lib/error-message.d.ts +7 -0
  347. package/dist/lib/format.d.ts +58 -4
  348. package/dist/lib/host-theme.d.ts +77 -0
  349. package/dist/lib/interaction-errors.d.ts +3 -0
  350. package/dist/lib/menu-styles.d.ts +22 -0
  351. package/dist/lib/notify-observer.d.ts +2 -0
  352. package/dist/lib/pierre-diffs-loader.d.ts +20 -0
  353. package/dist/lib/provider-recovery.d.ts +55 -0
  354. package/dist/lib/scrollbars.d.ts +11 -0
  355. package/dist/lib/terminal-input-readiness.d.ts +11 -0
  356. package/dist/lib/transient-retry.d.ts +25 -0
  357. package/dist/lib/workbench-peers.d.ts +36 -0
  358. package/dist/lib/xterm-theme.d.ts +7 -5
  359. package/dist/machines.js +9 -5
  360. package/dist/markdown-table-layout-3PV2UAAH.js +113 -0
  361. package/dist/markdown-table-layout-3PV2UAAH.js.map +1 -0
  362. package/dist/mcp-connection-status.d.ts +14 -0
  363. package/dist/model-mark-paths.d.ts +34 -0
  364. package/dist/model-mark-paths.js +16 -0
  365. package/dist/model-mark-paths.js.map +1 -0
  366. package/dist/model-picker-model.d.ts +111 -0
  367. package/dist/model-picker-model.js +25 -0
  368. package/dist/model-picker-model.js.map +1 -0
  369. package/dist/model-policy-picker-menu-7GJ5URPR.js +17 -0
  370. package/dist/model-policy-picker-menu-7GJ5URPR.js.map +1 -0
  371. package/dist/model-policy.d.ts +30 -3
  372. package/dist/model-policy.js +14 -1
  373. package/dist/native-previews.d.ts +28 -0
  374. package/dist/native-previews.js +82 -0
  375. package/dist/native-previews.js.map +1 -0
  376. package/dist/new-session-draft.d.ts +7 -0
  377. package/dist/new-session-draft.js +639 -0
  378. package/dist/new-session-draft.js.map +1 -0
  379. package/dist/notification-text.d.ts +27 -0
  380. package/dist/{noto-sans-arabic-loader-IA2T4X2A.js → noto-sans-arabic-loader-XSH2KYWJ.js} +3 -1
  381. package/dist/{noto-sans-arabic-loader-IA2T4X2A.js.map → noto-sans-arabic-loader-XSH2KYWJ.js.map} +1 -1
  382. package/dist/{noto-sans-jp-loader.generated-HFGLYBR3.js → noto-sans-jp-loader.generated-MR5L6V7K.js} +3 -1
  383. package/dist/{noto-sans-jp-loader.generated-HFGLYBR3.js.map → noto-sans-jp-loader.generated-MR5L6V7K.js.map} +1 -1
  384. package/dist/older-history.d.ts +3 -2
  385. package/dist/organization-model.d.ts +32 -0
  386. package/dist/organization-model.js +65 -0
  387. package/dist/organization-model.js.map +1 -0
  388. package/dist/{platform-activity-row-BJHUHXAT.js → platform-activity-row-5OCKOQWZ.js} +16 -78
  389. package/dist/platform-activity-row-5OCKOQWZ.js.map +1 -0
  390. package/dist/plugin-details.d.ts +14 -0
  391. package/dist/plugin-discovery.d.ts +25 -0
  392. package/dist/preview-loading-paint.d.ts +20 -0
  393. package/dist/provider.d.ts +25 -2
  394. package/dist/queue-presentation-model.d.ts +72 -0
  395. package/dist/queue-presentation-model.js +38 -0
  396. package/dist/queue-presentation-model.js.map +1 -0
  397. package/dist/{queue-surface-implementation-BSKUWHRI.js → queue-surface-implementation-GJBH7NKO.js} +65 -265
  398. package/dist/queue-surface-implementation-GJBH7NKO.js.map +1 -0
  399. package/dist/realtime/embedded-new-voice.d.ts +1 -0
  400. package/dist/realtime/embedded-voice.d.ts +1 -0
  401. package/dist/realtime/realtime-control.d.ts +4 -65
  402. package/dist/realtime/session-realtime.d.ts +78 -0
  403. package/dist/realtime.js +25 -1294
  404. package/dist/realtime.js.map +1 -1
  405. package/dist/sandbox-files-O27IPOZW.js +21 -0
  406. package/dist/sandbox-files-O27IPOZW.js.map +1 -0
  407. package/dist/sandbox-label-model.d.ts +30 -0
  408. package/dist/sandbox-label-model.js +20 -0
  409. package/dist/sandbox-label-model.js.map +1 -0
  410. package/dist/scrollbars.d.ts +1 -0
  411. package/dist/scrollbars.js +10 -0
  412. package/dist/scrollbars.js.map +1 -0
  413. package/dist/secret-reference-label.d.ts +14 -0
  414. package/dist/secrets-ui.d.ts +57 -0
  415. package/dist/session-agents-model.d.ts +10 -0
  416. package/dist/session-agents-model.js +25 -0
  417. package/dist/session-agents-model.js.map +1 -0
  418. package/dist/session-attention-model.d.ts +8 -0
  419. package/dist/session-attention-model.js +47 -0
  420. package/dist/session-attention-model.js.map +1 -0
  421. package/dist/session-auth-recommendation.d.ts +5 -0
  422. package/dist/session-capability-policy.d.ts +31 -0
  423. package/dist/session-feedback-model.d.ts +20 -0
  424. package/dist/session-feedback-model.js +56 -0
  425. package/dist/session-feedback-model.js.map +1 -0
  426. package/dist/session-list-model.d.ts +138 -0
  427. package/dist/session-list-model.js +274 -0
  428. package/dist/session-list-model.js.map +1 -0
  429. package/dist/session-realtime-7DJOIHQW.js +21 -0
  430. package/dist/session-realtime-7DJOIHQW.js.map +1 -0
  431. package/dist/session-realtime.d.ts +7 -0
  432. package/dist/session-realtime.js +19 -0
  433. package/dist/session-realtime.js.map +1 -0
  434. package/dist/session-status-model.d.ts +36 -0
  435. package/dist/session-ui.d.ts +39 -1
  436. package/dist/session-ui.js +126 -12
  437. package/dist/session-ui.js.map +1 -1
  438. package/dist/session.d.ts +8 -2
  439. package/dist/session.js +35 -20
  440. package/dist/site-snapshot.d.ts +14 -0
  441. package/dist/sites-ui.d.ts +24 -0
  442. package/dist/sites.d.ts +3 -0
  443. package/dist/sites.js +14 -0
  444. package/dist/sites.js.map +1 -0
  445. package/dist/skill-discovery.d.ts +47 -0
  446. package/dist/terminal.d.ts +8 -0
  447. package/dist/terminal.js +32 -0
  448. package/dist/terminal.js.map +1 -0
  449. package/dist/testing/session-scenarios.d.ts +10 -0
  450. package/dist/testing/session-scenarios.js +367 -0
  451. package/dist/testing/session-scenarios.js.map +1 -0
  452. package/dist/timeline/activity-rail.d.ts +11 -2
  453. package/dist/timeline/agent-identity.d.ts +30 -0
  454. package/dist/timeline/agent-row.d.ts +31 -0
  455. package/dist/timeline/allowance-exhausted-row.d.ts +24 -0
  456. package/dist/timeline/compaction-copy.d.ts +46 -0
  457. package/dist/timeline/entrance.d.ts +13 -1
  458. package/dist/timeline/focus-sequence.d.ts +9 -0
  459. package/dist/timeline/genie-copy.d.ts +3 -0
  460. package/dist/timeline/genie-loading.d.ts +47 -0
  461. package/dist/timeline/index.d.ts +10 -5
  462. package/dist/timeline/knowledge-receipt.d.ts +17 -0
  463. package/dist/timeline/message-sender.d.ts +19 -0
  464. package/dist/timeline/notice-presentation.d.ts +13 -0
  465. package/dist/timeline/parsers.d.ts +11 -0
  466. package/dist/timeline/patch-apply-command.d.ts +18 -0
  467. package/dist/timeline/platform-activity-presentation.d.ts +21 -0
  468. package/dist/timeline/platform-activity-row.d.ts +1 -2
  469. package/dist/timeline/presented-image.d.ts +11 -0
  470. package/dist/timeline/projection.d.ts +31 -2
  471. package/dist/timeline/provider-logo.d.ts +15 -0
  472. package/dist/timeline/question-nav-model.d.ts +23 -0
  473. package/dist/timeline/registry.d.ts +12 -1
  474. package/dist/timeline/retained-image.d.ts +18 -0
  475. package/dist/timeline/retained-loaders.d.ts +27 -0
  476. package/dist/timeline/rolling-activity.d.ts +11 -0
  477. package/dist/timeline/screenshot-lightbox.d.ts +23 -1
  478. package/dist/timeline/session-usage-limit-row.d.ts +11 -0
  479. package/dist/timeline/shared.d.ts +4 -1
  480. package/dist/timeline/startup-dispatch-details.d.ts +5 -0
  481. package/dist/timeline/startup-preference.d.ts +3 -0
  482. package/dist/timeline/startup-timings.d.ts +4 -0
  483. package/dist/timeline/tool-display-name.d.ts +5 -2
  484. package/dist/timeline/tool-presentation.d.ts +172 -0
  485. package/dist/timeline/turn-summary-model.d.ts +95 -0
  486. package/dist/timeline/turn-summary.d.ts +20 -54
  487. package/dist/timeline/types.d.ts +179 -7
  488. package/dist/timeline/work-presentation.d.ts +60 -0
  489. package/dist/timeline-annotation-markers-U3G66KUD.js +166 -0
  490. package/dist/timeline-annotation-markers-U3G66KUD.js.map +1 -0
  491. package/dist/timeline-annotation-selection-BI7QTPTZ.js +242 -0
  492. package/dist/timeline-annotation-selection-BI7QTPTZ.js.map +1 -0
  493. package/dist/timeline-annotations-dialog-FOMHSDTE.js +274 -0
  494. package/dist/timeline-annotations-dialog-FOMHSDTE.js.map +1 -0
  495. package/dist/timeline-model.d.ts +29 -0
  496. package/dist/timeline-model.js +254 -0
  497. package/dist/timeline-model.js.map +1 -0
  498. package/dist/usage/allowance-copy.d.ts +25 -0
  499. package/dist/usage/session-usage-limit.d.ts +88 -0
  500. package/dist/usage/summary.d.ts +35 -0
  501. package/dist/usage.d.ts +17 -0
  502. package/dist/usage.js +1187 -0
  503. package/dist/usage.js.map +1 -0
  504. package/dist/voice-recording-owner.d.ts +7 -0
  505. package/package.json +107 -7
  506. package/src/accounts.tsx +116 -14
  507. package/src/approvals.ts +8 -1
  508. package/src/artifacts-spreadsheet.ts +1 -0
  509. package/src/artifacts.ts +44 -0
  510. package/src/assets/README.md +8 -0
  511. package/src/assets/gmail.ico +0 -0
  512. package/src/assets/outlook.ico +0 -0
  513. package/src/capability-catalog-row.tsx +94 -0
  514. package/src/capability-logo-fallback.ts +22 -0
  515. package/src/client.ts +20 -31
  516. package/src/commands/registry.ts +18 -15
  517. package/src/commands/types.ts +2 -0
  518. package/src/components/anthropic-mark.tsx +10 -0
  519. package/src/components/approval-surface.tsx +401 -138
  520. package/src/components/artifacts/artifact-chrome.tsx +413 -0
  521. package/src/components/artifacts/artifact-sandbox.tsx +189 -0
  522. package/src/components/artifacts/artifact-surface.tsx +40 -20
  523. package/src/components/artifacts/chat-interactive-block.tsx +260 -0
  524. package/src/components/artifacts/deferred-chat-media.tsx +67 -0
  525. package/src/components/artifacts/document-editor.tsx +141 -16
  526. package/src/components/artifacts/editable-artifact-ui.tsx +4 -2
  527. package/src/components/artifacts/editable-artifact-view.tsx +197 -0
  528. package/src/components/artifacts/editable-artifact-workbench.tsx +5 -2
  529. package/src/components/artifacts/editable-document.tsx +37 -36
  530. package/src/components/artifacts/editable-presentation.tsx +36 -18
  531. package/src/components/artifacts/editable-spreadsheet.tsx +543 -103
  532. package/src/components/artifacts/index.ts +45 -0
  533. package/src/components/artifacts/inline-html-document.ts +24 -0
  534. package/src/components/artifacts/latest-task-runner.ts +49 -0
  535. package/src/components/artifacts/presentation-editor.tsx +323 -67
  536. package/src/components/artifacts/published-html-artifact-frame.tsx +122 -5
  537. package/src/components/artifacts/session-artifact-viewer.tsx +378 -0
  538. package/src/components/artifacts/site-view.tsx +121 -0
  539. package/src/components/artifacts/spreadsheet-canvas.ts +5 -10
  540. package/src/components/artifacts/spreadsheet-command-queue.ts +184 -0
  541. package/src/components/artifacts/spreadsheet-download.tsx +109 -0
  542. package/src/components/artifacts/spreadsheet-general-display.ts +43 -0
  543. package/src/components/artifacts/spreadsheet-grid.tsx +344 -144
  544. package/src/components/artifacts/spreadsheet-resize.tsx +421 -0
  545. package/src/components/artifacts/spreadsheet-submissions.tsx +46 -0
  546. package/src/components/artifacts/visualization-assets/base.css +1015 -0
  547. package/src/components/artifacts/visualization-assets/helpers.html +349 -0
  548. package/src/components/artifacts/visualization-assets.gen.ts +3 -0
  549. package/src/components/browser-select-control.tsx +312 -0
  550. package/src/components/browser-viewer.tsx +778 -318
  551. package/src/components/chat-composer.tsx +47 -5
  552. package/src/components/child-session-link.tsx +36 -0
  553. package/src/components/claude-mark.tsx +11 -0
  554. package/src/components/code-editor.tsx +13 -16
  555. package/src/components/composer-transcription-control.tsx +128 -30
  556. package/src/components/composer.tsx +404 -195
  557. package/src/components/computer-viewer.tsx +880 -253
  558. package/src/components/copy-button.tsx +13 -5
  559. package/src/components/desktop-viewer.tsx +1 -1
  560. package/src/components/enrollment-consent.tsx +1 -1
  561. package/src/components/enrollment-device-flow.tsx +1 -1
  562. package/src/components/file-browser.tsx +10 -14
  563. package/src/components/fleet-tile.tsx +9 -18
  564. package/src/components/generated-video-player.tsx +4 -2
  565. package/src/components/grok-mark.tsx +34 -0
  566. package/src/components/human-input-form.tsx +590 -283
  567. package/src/components/human-input-surface.tsx +11 -0
  568. package/src/components/interaction-intervention-banner.tsx +2 -2
  569. package/src/components/linkified-text.tsx +79 -0
  570. package/src/components/machine-card.tsx +35 -78
  571. package/src/components/machine-dock-bar.tsx +1 -1
  572. package/src/components/machine-input-display.ts +197 -3
  573. package/src/components/machines/agent-status-icon.tsx +31 -0
  574. package/src/components/machines/machine-detail.tsx +200 -31
  575. package/src/components/machines/machine-operation-policy.tsx +1 -1
  576. package/src/components/machines/machine-status.ts +241 -0
  577. package/src/components/machines-dashboard.tsx +2 -17
  578. package/src/components/markdown-table-layout.ts +143 -0
  579. package/src/components/markdown.tsx +466 -129
  580. package/src/components/message-timeline.tsx +2567 -728
  581. package/src/components/model-mark.tsx +102 -0
  582. package/src/components/model-picker.tsx +15 -4
  583. package/src/components/model-policy-picker-menu.tsx +371 -0
  584. package/src/components/model-policy-picker.tsx +256 -412
  585. package/src/components/new-conversation.tsx +221 -0
  586. package/src/components/open-geni-chat.tsx +309 -0
  587. package/src/components/open-geni-links.tsx +179 -0
  588. package/src/components/openrouter-mark.tsx +13 -0
  589. package/src/components/opper-mark.tsx +18 -0
  590. package/src/components/pierre-diff.tsx +41 -16
  591. package/src/components/pierre-file.tsx +35 -10
  592. package/src/components/preview-loading.tsx +109 -0
  593. package/src/components/provider-recovery-notice.tsx +49 -0
  594. package/src/components/queue-item-content.ts +14 -0
  595. package/src/components/queue-surface-implementation.tsx +51 -356
  596. package/src/components/queue-surface-state.tsx +3 -1
  597. package/src/components/rehype-secret-refs.ts +98 -0
  598. package/src/components/remark-soft-line-breaks.ts +166 -0
  599. package/src/components/sandbox-files.tsx +91 -15
  600. package/src/components/sandbox-terminal.tsx +84 -13
  601. package/src/components/sandbox-workspace.tsx +147 -51
  602. package/src/components/secret-question-field.tsx +324 -0
  603. package/src/components/secret-ref-chip.tsx +185 -0
  604. package/src/components/session-capability-frame.tsx +197 -0
  605. package/src/components/session-chrome.tsx +842 -251
  606. package/src/components/session-commands-panel.tsx +126 -0
  607. package/src/components/session-commands.tsx +18 -0
  608. package/src/components/session-connection-request.tsx +97 -0
  609. package/src/components/session-conversation.tsx +876 -0
  610. package/src/components/session-list.tsx +257 -0
  611. package/src/components/session-mcp-capability-card.tsx +680 -0
  612. package/src/components/session-proxy-scope.tsx +153 -0
  613. package/src/components/session-status.tsx +26 -17
  614. package/src/components/session-usage-limit-notice.tsx +174 -0
  615. package/src/components/timeline-anchor.tsx +119 -0
  616. package/src/components/timeline-annotation-chrome.tsx +196 -0
  617. package/src/components/timeline-annotation-layout.ts +268 -0
  618. package/src/components/timeline-annotation-markers.tsx +185 -0
  619. package/src/components/timeline-annotation-reveal-context.ts +9 -0
  620. package/src/components/timeline-annotation-selection.tsx +106 -54
  621. package/src/components/timeline-annotation-shared.ts +245 -0
  622. package/src/components/timeline-annotations-dialog.tsx +185 -128
  623. package/src/components/timeline-annotations.tsx +189 -45
  624. package/src/components/timeline-reading-progress.ts +80 -0
  625. package/src/components/timeline-search.tsx +168 -0
  626. package/src/components/timeline-settlement.ts +60 -0
  627. package/src/components/tip-follow.ts +52 -0
  628. package/src/components/tool-action-review.tsx +650 -0
  629. package/src/components/tool-review-history.tsx +88 -0
  630. package/src/components/tooltip.tsx +38 -5
  631. package/src/components/usage-limit-notice.tsx +179 -0
  632. package/src/components/usage-member-list.tsx +792 -0
  633. package/src/components/usage-meter.tsx +382 -0
  634. package/src/components/use-viewer-menu-dismiss.ts +33 -0
  635. package/src/components/user-message-body.tsx +27 -2
  636. package/src/components/workspace-dock.tsx +18 -1
  637. package/src/composer-messages.ts +150 -0
  638. package/src/composer-voice-events.ts +12 -0
  639. package/src/composer.ts +6 -1
  640. package/src/connect-account-identity.tsx +34 -0
  641. package/src/connect-accounts.tsx +233 -0
  642. package/src/connect-chooser.tsx +336 -0
  643. package/src/connect-panel.tsx +99 -0
  644. package/src/connect-setup.tsx +355 -0
  645. package/src/connect.ts +60 -0
  646. package/src/connection-assets.d.ts +4 -0
  647. package/src/connection-catalog.tsx +162 -0
  648. package/src/connection-discovery.tsx +238 -0
  649. package/src/connection-installed.tsx +51 -0
  650. package/src/connection-logo.tsx +37 -0
  651. package/src/connection-service-logo.tsx +55 -0
  652. package/src/connection-service-presentation.ts +46 -0
  653. package/src/connection-skeleton.tsx +16 -0
  654. package/src/connection-type-picker.tsx +36 -0
  655. package/src/conversation-timeline.ts +92 -0
  656. package/src/desktop.ts +24 -0
  657. package/src/device-authorization.tsx +98 -0
  658. package/src/diffs.ts +16 -0
  659. package/src/discovery-cache.ts +42 -0
  660. package/src/editor.ts +13 -0
  661. package/src/hooks/internal.ts +3 -2
  662. package/src/hooks/latest-question.ts +164 -0
  663. package/src/hooks/shared-machines-list.ts +34 -1
  664. package/src/hooks/shared-query.ts +721 -0
  665. package/src/hooks/use-attached-browsers.ts +1 -1
  666. package/src/hooks/use-auth-runs.ts +0 -27
  667. package/src/hooks/use-available-models.ts +21 -15
  668. package/src/hooks/use-browser-frame-stream.ts +19 -2
  669. package/src/hooks/use-browser-session.ts +680 -205
  670. package/src/hooks/use-browser-sessions.ts +32 -1
  671. package/src/hooks/use-client-config-flags.ts +118 -0
  672. package/src/hooks/use-codex-accounts.ts +247 -47
  673. package/src/hooks/use-composer.ts +276 -47
  674. package/src/hooks/use-computer-frame-stream.ts +25 -7
  675. package/src/hooks/use-computer-input-posture.ts +94 -0
  676. package/src/hooks/use-computer-session.ts +409 -95
  677. package/src/hooks/use-connect.ts +23 -0
  678. package/src/hooks/use-desktop-stream.ts +19 -28
  679. package/src/hooks/use-file-attachments.ts +80 -4
  680. package/src/hooks/use-goal.ts +107 -13
  681. package/src/hooks/use-last-started-turn-policy.ts +46 -14
  682. package/src/hooks/use-machines.ts +4 -0
  683. package/src/hooks/use-new-conversation.ts +374 -0
  684. package/src/hooks/use-new-session-draft.ts +813 -0
  685. package/src/hooks/use-realtime-voice-models.ts +46 -0
  686. package/src/hooks/use-sandbox-files.ts +68 -20
  687. package/src/hooks/use-session-background-commands.ts +22 -11
  688. package/src/hooks/use-session-capabilities.ts +3 -2
  689. package/src/hooks/use-session-conversation.ts +150 -0
  690. package/src/hooks/use-session-events.ts +703 -382
  691. package/src/hooks/use-session-lineage.ts +20 -16
  692. package/src/hooks/use-session-mcp-approval-policy.ts +13 -5
  693. package/src/hooks/use-session-model-account.ts +174 -0
  694. package/src/hooks/use-session.ts +53 -21
  695. package/src/hooks/use-slash-commands.ts +6 -11
  696. package/src/hooks/use-supergrok-accounts.ts +22 -8
  697. package/src/hooks/use-terminal-stream.ts +153 -27
  698. package/src/hooks/use-transcription.ts +6 -0
  699. package/src/hooks/use-turn-queue.ts +2 -1
  700. package/src/hooks/use-usage.ts +100 -0
  701. package/src/hooks/use-vaults.ts +153 -0
  702. package/src/hooks/use-video-artifact-playback.ts +1 -1
  703. package/src/hooks/use-voice-input.ts +127 -19
  704. package/src/hooks/use-workspace-capture.ts +3 -2
  705. package/src/hooks/use-workspace-edit.ts +23 -5
  706. package/src/hooks/use-workspace-sessions.ts +97 -32
  707. package/src/human-input-model.ts +359 -0
  708. package/src/identity-link-accounts.tsx +148 -0
  709. package/src/identity-link-consent.tsx +192 -0
  710. package/src/index.ts +262 -16
  711. package/src/interaction.ts +1 -0
  712. package/src/lib/clipboard.ts +29 -0
  713. package/src/lib/error-message.ts +37 -0
  714. package/src/lib/format.ts +213 -16
  715. package/src/lib/host-theme.ts +529 -0
  716. package/src/lib/interaction-errors.ts +15 -0
  717. package/src/lib/menu-styles.ts +44 -0
  718. package/src/lib/notify-observer.ts +15 -0
  719. package/src/lib/pierre-diffs-loader.ts +53 -0
  720. package/src/lib/provider-recovery.ts +243 -0
  721. package/src/lib/scrollbars.ts +30 -0
  722. package/src/lib/terminal-input-readiness.ts +51 -0
  723. package/src/lib/transient-retry.ts +99 -0
  724. package/src/lib/workbench-peers.ts +62 -0
  725. package/src/lib/xterm-theme.ts +23 -21
  726. package/src/mcp-connection-status.ts +35 -0
  727. package/src/model-mark-paths.ts +53 -0
  728. package/src/model-picker-model.ts +330 -0
  729. package/src/model-policy.ts +299 -74
  730. package/src/native-previews.ts +100 -0
  731. package/src/new-session-draft.ts +22 -0
  732. package/src/notification-text.ts +81 -0
  733. package/src/older-history.ts +22 -4
  734. package/src/organization-model.ts +118 -0
  735. package/src/plugin-details.tsx +185 -0
  736. package/src/plugin-discovery.tsx +196 -0
  737. package/src/preview-loading-paint.ts +68 -0
  738. package/src/provider.tsx +74 -15
  739. package/src/queue-presentation-model.ts +464 -0
  740. package/src/realtime/dropdown-menu.tsx +19 -12
  741. package/src/realtime/embedded-new-voice.tsx +1 -0
  742. package/src/realtime/embedded-voice.tsx +3 -0
  743. package/src/realtime/realtime-control.tsx +138 -501
  744. package/src/realtime/session-realtime.ts +566 -0
  745. package/src/sandbox-label-model.ts +53 -0
  746. package/src/scrollbars.ts +3 -0
  747. package/src/secret-reference-label.ts +43 -0
  748. package/src/secrets-ui.tsx +59 -0
  749. package/src/session-agents-model.ts +33 -0
  750. package/src/session-attention-model.ts +62 -0
  751. package/src/session-auth-recommendation.ts +48 -0
  752. package/src/session-capability-policy.ts +196 -0
  753. package/src/session-context.ts +3 -4
  754. package/src/session-feedback-model.ts +95 -0
  755. package/src/session-list-model.ts +434 -0
  756. package/src/session-realtime.ts +18 -0
  757. package/src/session-status-model.ts +99 -0
  758. package/src/session-ui.ts +98 -1
  759. package/src/session.ts +17 -1
  760. package/src/site-snapshot.ts +74 -0
  761. package/src/sites-ui.tsx +348 -0
  762. package/src/sites.ts +3 -0
  763. package/src/skill-discovery.tsx +208 -0
  764. package/src/terminal.ts +26 -0
  765. package/src/testing/session-scenarios.ts +404 -0
  766. package/src/timeline/activity-rail.tsx +242 -101
  767. package/src/timeline/agent-identity.tsx +84 -0
  768. package/src/timeline/agent-row.tsx +231 -0
  769. package/src/timeline/allowance-exhausted-row.tsx +86 -0
  770. package/src/timeline/compaction-copy.ts +115 -0
  771. package/src/timeline/entrance.tsx +28 -3
  772. package/src/timeline/fleet-decision-projection.ts +2 -1
  773. package/src/timeline/fleet-decision-row.tsx +1 -0
  774. package/src/timeline/focus-sequence.ts +44 -0
  775. package/src/timeline/genie-copy.ts +19 -0
  776. package/src/timeline/genie-loading.tsx +143 -0
  777. package/src/timeline/index.ts +30 -2
  778. package/src/timeline/knowledge-receipt.tsx +100 -0
  779. package/src/timeline/message-sender.tsx +71 -0
  780. package/src/timeline/notice-presentation.ts +48 -0
  781. package/src/timeline/parsers.ts +42 -2
  782. package/src/timeline/patch-apply-command.tsx +135 -0
  783. package/src/timeline/platform-activity-presentation.ts +166 -0
  784. package/src/timeline/platform-activity-row.tsx +15 -90
  785. package/src/timeline/presented-image.ts +63 -0
  786. package/src/timeline/projection.ts +1746 -76
  787. package/src/timeline/provider-logo.tsx +53 -0
  788. package/src/timeline/question-nav-model.ts +28 -0
  789. package/src/timeline/registry.ts +26 -4
  790. package/src/timeline/retained-image.ts +97 -0
  791. package/src/timeline/retained-loaders.ts +91 -0
  792. package/src/timeline/rolling-activity.tsx +80 -0
  793. package/src/timeline/screenshot-lightbox.tsx +228 -5
  794. package/src/timeline/session-usage-limit-row.tsx +42 -0
  795. package/src/timeline/shared.tsx +42 -15
  796. package/src/timeline/startup-dispatch-details.tsx +36 -0
  797. package/src/timeline/startup-preference.ts +33 -0
  798. package/src/timeline/startup-timings.tsx +145 -0
  799. package/src/timeline/tool-display-name.ts +19 -7
  800. package/src/timeline/tool-presentation.ts +1316 -0
  801. package/src/timeline/tool-renderers.tsx +835 -1138
  802. package/src/timeline/turn-summary-model.ts +343 -0
  803. package/src/timeline/turn-summary.tsx +272 -365
  804. package/src/timeline/types.ts +189 -6
  805. package/src/timeline/work-presentation.ts +189 -0
  806. package/src/timeline-model.ts +143 -0
  807. package/src/usage/allowance-copy.ts +64 -0
  808. package/src/usage/session-usage-limit.ts +266 -0
  809. package/src/usage/summary.ts +93 -0
  810. package/src/usage.ts +70 -0
  811. package/src/voice-recording-owner.ts +22 -0
  812. package/styles/compiled.css +2319 -748
  813. package/styles/connect.css +290 -0
  814. package/styles/connect.d.ts +1 -0
  815. package/styles/effective-tokens.css +1 -0
  816. package/styles/index.css +309 -9
  817. package/styles/responsive.css +99 -7
  818. package/styles/session-capability.css +44 -0
  819. package/styles/tokens.css +134 -66
  820. package/dist/browser-viewer-AEFHUYZK.js +0 -14
  821. package/dist/chunk-4MDLEHXL.js.map +0 -1
  822. package/dist/chunk-5YK2ZDIG.js.map +0 -1
  823. package/dist/chunk-D3UIAJKY.js.map +0 -1
  824. package/dist/chunk-ELIYWBZR.js.map +0 -1
  825. package/dist/chunk-H5YGYNXS.js.map +0 -1
  826. package/dist/chunk-HQVCKCLG.js +0 -9458
  827. package/dist/chunk-HQVCKCLG.js.map +0 -1
  828. package/dist/chunk-HUIKIDM5.js.map +0 -1
  829. package/dist/chunk-ICA6R6TW.js.map +0 -1
  830. package/dist/chunk-JVYQRRC4.js.map +0 -1
  831. package/dist/chunk-KCGXPP5V.js.map +0 -1
  832. package/dist/chunk-KZ73RL76.js.map +0 -1
  833. package/dist/chunk-LAKLF4PA.js +0 -141
  834. package/dist/chunk-LAKLF4PA.js.map +0 -1
  835. package/dist/chunk-MZ2G2XBA.js.map +0 -1
  836. package/dist/chunk-NEXLLTP4.js.map +0 -1
  837. package/dist/chunk-OAM4WAAM.js.map +0 -1
  838. package/dist/chunk-PEI6RANI.js.map +0 -1
  839. package/dist/chunk-RGALCTWO.js.map +0 -1
  840. package/dist/chunk-W6NFJXSA.js +0 -454
  841. package/dist/chunk-W6NFJXSA.js.map +0 -1
  842. package/dist/chunk-WSK7G5LE.js +0 -170
  843. package/dist/chunk-WSK7G5LE.js.map +0 -1
  844. package/dist/chunk-X4A66EWZ.js +0 -223
  845. package/dist/chunk-X4A66EWZ.js.map +0 -1
  846. package/dist/chunk-XXWH2JLC.js.map +0 -1
  847. package/dist/chunk-YFKJ7VZH.js +0 -268
  848. package/dist/chunk-YFKJ7VZH.js.map +0 -1
  849. package/dist/chunk-YJFGAFBD.js.map +0 -1
  850. package/dist/computer-viewer-FWEZISW4.js +0 -16
  851. package/dist/fleet-decision-row-GGCAT4E4.js.map +0 -1
  852. package/dist/hooks/use-environments.d.ts +0 -41
  853. package/dist/hooks/use-packs.d.ts +0 -32
  854. package/dist/platform-activity-row-BJHUHXAT.js.map +0 -1
  855. package/dist/queue-surface-implementation-BSKUWHRI.js.map +0 -1
  856. package/dist/timeline-annotation-selection-3L7LHGG2.js +0 -187
  857. package/dist/timeline-annotation-selection-3L7LHGG2.js.map +0 -1
  858. package/dist/timeline-annotations-dialog-U7EVLR75.js +0 -196
  859. package/dist/timeline-annotations-dialog-U7EVLR75.js.map +0 -1
  860. package/src/hooks/use-environments.ts +0 -182
  861. package/src/hooks/use-packs.ts +0 -157
  862. /package/dist/{browser-viewer-AEFHUYZK.js.map → browser-viewer-ZWR6MWJ7.js.map} +0 -0
  863. /package/dist/{computer-viewer-FWEZISW4.js.map → chat-interactive-block-BO2IWXVK.js.map} +0 -0
@@ -1,11 +1,18 @@
1
- import type { SessionEvent, SessionStatus, StreamConnectionState } from "@opengeni/sdk";
2
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
1
+ import {
2
+ sessionEventStreamCoveredThrough,
3
+ type SessionEvent,
4
+ type SessionStatus,
5
+ type StreamConnectionState,
6
+ } from "@opengeni/sdk";
7
+ import { startTransition, useCallback, useEffect, useMemo, useRef, useState } from "react";
3
8
  import { useEmbeddedSession, type EmbeddedSessionClientOverride } from "../session-context";
4
9
  import { createOlderHistoryLoadReceipt, type OlderHistoryLoadReceipt } from "../older-history";
5
10
  import { buildTimeline, groupTimeline, sessionStatusFromEvents } from "../timeline/projection";
6
11
  import type { TimelineItem } from "../timeline/types";
7
12
  import type { EmbeddedSessionClientLike } from "../client";
13
+ import { normalizeError } from "../lib/error-message";
8
14
  import { usePageLiveActivity } from "./internal";
15
+ import type { LatestQuestionOptions } from "./latest-question";
9
16
 
10
17
  export type SessionEventsConnectionState = StreamConnectionState | "idle" | "ended" | "error";
11
18
 
@@ -21,10 +28,12 @@ export type UseSessionEventsOptions = EmbeddedSessionClientOverride & {
21
28
  export type UseSessionEventsResult = {
22
29
  /** Replayed + live events, ordered by sequence, no gaps, no duplicates. */
23
30
  events: SessionEvent[];
24
- /** Projected, renderable timeline (memoized over `events`). */
31
+ /** Renderable timeline, including the bounded navigation witness for a distant queued prompt. */
25
32
  timeline: TimelineItem[];
26
33
  /** Latest session status observed in the event log, if any. */
27
34
  sessionStatus: SessionStatus | null;
35
+ /** Sequence of the retained status projection, including events evicted from the window. */
36
+ sessionStatusSequence?: number;
28
37
  connectionState: SessionEventsConnectionState;
29
38
  /** Highest sequence seen so far (0 before the first event). */
30
39
  lastSequence: number;
@@ -34,6 +43,10 @@ export type UseSessionEventsResult = {
34
43
  windowTruncated: boolean;
35
44
  /** True until the initial tail window has been applied (windowed mode). */
36
45
  initialLoading: boolean;
46
+ /** A history window (including an empty tail) succeeded for this session/replay
47
+ * identity. Stays true through later stream errors and navigation/reloads.
48
+ * Full replay has no snapshot-completion watermark and does not set this by itself. */
49
+ initialHistoryReady: boolean;
37
50
  /** Whether older durable events are available before the current window. */
38
51
  hasOlder: boolean;
39
52
  /** True while an older window is being fetched. */
@@ -66,13 +79,20 @@ export type UseSessionEventsResult = {
66
79
  * "Jump to latest" when the tip is not in memory (history view).
67
80
  */
68
81
  jumpToLatest: () => Promise<void>;
82
+ /** Resolve the newest eligible durable human question. Pending prompts use the
83
+ * optional queue destination and return null; started prompts load their actual
84
+ * turn context (pass `timeline` to MessageTimeline as `items`). */
85
+ jumpToLatestQuestion: (options?: LatestQuestionOptions) => Promise<number | null>;
86
+ /** Replace history with a bounded window containing this exact durable event. */
87
+ jumpToSequence: (sequence: number, options?: { signal?: AbortSignal }) => Promise<boolean>;
88
+ loadingTarget: boolean;
69
89
  error: Error | null;
70
90
  };
71
91
 
72
- const INITIAL_TAIL_PAGE_SIZE = 1000;
73
- const OLDER_PAGE_SIZE = 5000;
74
- const NEWER_PAGE_SIZE = 5000;
75
- const OLDEST_PAGE_SIZE = 1000;
92
+ // Keep every browser history read inside one database batch, including the
93
+ // server's one-row continuation lookahead. A large total session must never
94
+ // turn one lazy page into dozens of sequential database round trips.
95
+ const SESSION_HISTORY_PAGE_SIZE = 1000;
76
96
  const INITIAL_FETCH_CAP = 1;
77
97
  const OLDER_GROUP_TARGET = 32;
78
98
  const OLDER_FETCH_CAP = 2;
@@ -80,37 +100,26 @@ const NEWER_GROUP_TARGET = 32;
80
100
  const NEWER_FETCH_CAP = 2;
81
101
  const OLDEST_GROUP_TARGET = 32;
82
102
  const OLDEST_FETCH_CAP = 2;
83
- const BOUNDARY_PAGE_CAP = 4;
103
+ // A tail page may land inside one unusually dense turn. Permit exactly one
104
+ // additional bounded page to find its user/session boundary without turning a
105
+ // fresh open into an unbounded history walk.
106
+ const BOUNDARY_PAGE_CAP = 1;
107
+ // Foreground reconciliation is intentionally semantic, not merely time-based:
108
+ // tiny raw gaps can stay on SSE; medium raw gaps get one compact probe so a
109
+ // token-heavy single answer is not mistaken for hundreds of visible messages;
110
+ // only a large/complex missed window reloads the latest tail.
111
+ const FOREGROUND_DIRECT_REPLAY_MAX_SEQUENCES = 16;
112
+ const FOREGROUND_COMPACT_PROBE_MAX_SEQUENCES = SESSION_HISTORY_PAGE_SIZE;
113
+ const FOREGROUND_COMPACT_CATCHUP_MAX_EVENTS = 128;
114
+ const FOREGROUND_COMPACT_CATCHUP_MAX_GROUPS = 16;
115
+ const FOREGROUND_COMPACT_CATCHUP_MAX_BYTES = 512 * 1024;
84
116
  const EMPTY_EVENTS: SessionEvent[] = [];
85
117
  const encoder = new TextEncoder();
86
- const decoder = new TextDecoder();
87
-
88
- const BROWSER_EVENT_TYPE_MAX_BYTES = 256;
89
- const BROWSER_EVENT_ID_MAX_BYTES = 256;
90
- const BROWSER_EVENT_CLIENT_ID_MAX_BYTES = 4 * 1024;
91
- const BROWSER_EVENT_DUPLICATE_REASON_MAX_BYTES = 4 * 1024;
92
- const BROWSER_EVENT_PAYLOAD_PREVIEW_MAX_BYTES = 48 * 1024;
93
- const BROWSER_EVENT_PAYLOAD_IDENTITY_FIELDS = [
94
- "id",
95
- "callId",
96
- "call_id",
97
- "name",
98
- "toolName",
99
- "status",
100
- "code",
101
- "isError",
102
- "stream",
103
- "commandId",
104
- "sequence",
105
- "coalescedUntil",
106
- "coalescedCount",
107
- "firstSequence",
108
- "lastSequence",
109
- ] as const;
110
-
111
- export const SESSION_EVENT_BROWSER_MAX_BYTES = 8 * 1024 * 1024;
112
- export const SESSION_EVENT_BROWSER_MAX_COUNT = 10_000;
113
- export const SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES = 96 * 1024;
118
+ // Durable history remains on the server and is available through bounded
119
+ // navigation. Keep the live browser working set small across multiple tabs;
120
+ // serialized payloads also expand into objects and timeline projections.
121
+ export const SESSION_EVENT_BROWSER_MAX_BYTES = 16 * 1024 * 1024;
122
+ export const SESSION_EVENT_BROWSER_MAX_COUNT = 20_000;
114
123
  export const SESSION_EVENT_BROWSER_PENDING_MAX_BYTES = 1024 * 1024;
115
124
  export const SESSION_EVENT_BROWSER_PENDING_MAX_COUNT = 256;
116
125
 
@@ -143,20 +152,47 @@ export function useSessionEvents(
143
152
  const replay = options.replay ?? "windowed";
144
153
  const fullReplay = replay === "full" || after !== 0;
145
154
  const streamKey = `${workspaceId}\u0000${sessionId ?? ""}\u0000${after}\u0000${fullReplay ? "full" : "windowed"}`;
155
+ // Bind callbacks to this read lifetime, including a return to a previous
156
+ // identity. Publish during render so old callbacks are fenced before passive
157
+ // cleanup can advance the navigation generation.
158
+ const navigationIdentityRef = useRef({ client, streamKey, enabled });
159
+ if (
160
+ navigationIdentityRef.current.client !== client ||
161
+ navigationIdentityRef.current.streamKey !== streamKey ||
162
+ navigationIdentityRef.current.enabled !== enabled
163
+ ) {
164
+ navigationIdentityRef.current = { client, streamKey, enabled };
165
+ }
166
+ const navigationIdentity = navigationIdentityRef.current;
167
+ const ownsNavigation = useCallback(
168
+ () => navigationIdentityRef.current === navigationIdentity,
169
+ [navigationIdentity],
170
+ );
146
171
 
147
172
  const [eventWindow, setEventWindow] = useState<BrowserSessionEventWindow>(EMPTY_EVENT_WINDOW);
173
+ // One navigation witness, not a second history cache. Raw events stay contiguous.
174
+ const [questionEvidence, setQuestionEvidence] = useState<{
175
+ client: EmbeddedSessionClientLike;
176
+ streamKey: string;
177
+ anchor: number;
178
+ events: SessionEvent[];
179
+ } | null>(null);
148
180
  const [connectionState, setConnectionState] = useState<SessionEventsConnectionState>("idle");
149
181
  const [error, setError] = useState<Error | null>(null);
182
+ const [newerError, setNewerError] = useState<Error | null>(null);
150
183
  const [hasOlder, setHasOlder] = useState(false);
151
184
  const [hasNewer, setHasNewer] = useState(false);
152
185
  const [sessionStatusProjection, setSessionStatusProjection] = useState<SessionStatus | null>(
153
186
  null,
154
187
  );
155
188
  const [initialLoading, setInitialLoading] = useState(true);
189
+ const [initialHistoryReady, setInitialHistoryReady] = useState(false);
156
190
  const [loadingOlder, setLoadingOlder] = useState(false);
157
191
  const [loadingNewer, setLoadingNewer] = useState(false);
158
192
  const [loadingOldest, setLoadingOldest] = useState(false);
159
193
  const [loadingLatest, setLoadingLatest] = useState(false);
194
+ const [loadingTarget, setLoadingTarget] = useState(false);
195
+ const loadingTargetRef = useRef(false);
160
196
  const [streamEpoch, setStreamEpoch] = useState(0);
161
197
  // "history" = reading a non-tip window; live SSE must not fill the gap.
162
198
  const [viewMode, setViewMode] = useState<"live" | "history">("live");
@@ -172,9 +208,11 @@ export function useSessionEvents(
172
208
  const loadingLatestRef = useRef(false);
173
209
  const viewModeRef = useRef<"live" | "history">("live");
174
210
  const initialWindowLoadedRef = useRef(false);
211
+ const reconcileAfterPageResumeRef = useRef(false);
175
212
  const streamAbortRef = useRef<AbortController | null>(null);
176
213
  const streamKeyRef = useRef<string | null>(null);
177
214
  const generationRef = useRef(0);
215
+ const navigationGenerationRef = useRef(0);
178
216
  const eventWindowRef = useRef<BrowserSessionEventWindow>(EMPTY_EVENT_WINDOW);
179
217
  const sessionStatusRef = useRef<{
180
218
  sequence: number;
@@ -187,6 +225,25 @@ export function useSessionEvents(
187
225
  // new stream identity cannot expose the previous session's event log.
188
226
  const [stateStreamKey, setStateStreamKey] = useState(streamKey);
189
227
 
228
+ // Navigation belongs to the session/client lifetime, not the transport: an
229
+ // SSE reconnect must never cancel an in-flight history page.
230
+ useEffect(() => {
231
+ navigationGenerationRef.current += 1;
232
+ setNewerError(null);
233
+ loadingOlderRef.current = false;
234
+ loadingNewerRef.current = false;
235
+ loadingOldestRef.current = false;
236
+ loadingTargetRef.current = false;
237
+ setQuestionEvidence(null);
238
+ setLoadingTarget(false);
239
+ setLoadingOlder(false);
240
+ setLoadingNewer(false);
241
+ setLoadingOldest(false);
242
+ return () => {
243
+ navigationGenerationRef.current += 1;
244
+ };
245
+ }, [client, workspaceId, sessionId, after, enabled, fullReplay]);
246
+
190
247
  useEffect(() => {
191
248
  // Reset the accumulated log only when the stream identity changes —
192
249
  // pausing via `enabled: false` keeps the timeline visible.
@@ -205,6 +262,7 @@ export function useSessionEvents(
205
262
  setLoadingOldest(false);
206
263
  setLoadingLatest(false);
207
264
  setInitialLoading(true);
265
+ setInitialHistoryReady(false);
208
266
  lastSequenceRef.current = after;
209
267
  streamResumeSequenceRef.current = after;
210
268
  oldestSequenceRef.current = null;
@@ -218,29 +276,50 @@ export function useSessionEvents(
218
276
  viewModeRef.current = "live";
219
277
  setViewMode("live");
220
278
  initialWindowLoadedRef.current = false;
279
+ reconcileAfterPageResumeRef.current = false;
221
280
  }
222
281
  // AbortController is advisory: custom SDK clients and async iterators may
223
282
  // ignore it and resolve/yield after cleanup. Fence every effect instance so
224
283
  // only the newest dependency generation can mutate refs or React state.
225
284
  const generation = generationRef.current + 1;
226
285
  generationRef.current = generation;
227
- if (loadingOlderRef.current) {
228
- loadingOlderRef.current = false;
229
- setLoadingOlder(false);
230
- }
231
286
  if (!sessionId || !streamEnabled) {
287
+ // A page that stayed hidden beyond the live-activity grace deliberately
288
+ // closed its SSE connection. Replaying from the old cursor on return can
289
+ // drip a large background backlog through React in many fast batches and
290
+ // leave the pinned camera catching up long after the user is foregrounded.
291
+ // Remember that suspension so the next live effect replaces the browser
292
+ // window with one compact durable tail instead.
293
+ if (
294
+ sessionId &&
295
+ enabled &&
296
+ !pageLive &&
297
+ !fullReplay &&
298
+ viewModeRef.current === "live" &&
299
+ initialWindowLoadedRef.current
300
+ ) {
301
+ reconcileAfterPageResumeRef.current = true;
302
+ }
232
303
  setConnectionState("idle");
233
304
  return;
234
305
  }
235
306
  // History view owns a non-tip window; do not open SSE (it would replay the
236
307
  // entire gap into the browser). jumpToLatest / catching loadNewer resume live.
237
308
  if (viewMode === "history") {
309
+ reconcileAfterPageResumeRef.current = false;
238
310
  setConnectionState("idle");
239
311
  setInitialLoading(false);
240
312
  return;
241
313
  }
314
+ const reconcileForegroundResume = reconcileAfterPageResumeRef.current && !fullReplay;
315
+ reconcileAfterPageResumeRef.current = false;
242
316
  const controller = new AbortController();
243
- const isCurrent = () => generationRef.current === generation && !controller.signal.aborted;
317
+ const isCurrent = () =>
318
+ generationRef.current === generation &&
319
+ !controller.signal.aborted &&
320
+ navigationIdentityRef.current.client === client &&
321
+ navigationIdentityRef.current.streamKey === streamKey &&
322
+ navigationIdentityRef.current.enabled === enabled;
244
323
  streamAbortRef.current = controller;
245
324
  // Batch yielded events into one React update per flush window so a long
246
325
  // replay (thousands of events) does not render per event. Project every
@@ -283,9 +362,12 @@ export function useSessionEvents(
283
362
  batchResumeSequence,
284
363
  );
285
364
  }
286
- observeSessionStatus(batch, sessionStatusRef, setSessionStatusProjection);
365
+ const status = observeSessionStatus(batch, sessionStatusRef);
366
+ if (status !== undefined) {
367
+ setSessionStatusProjection(status);
368
+ }
287
369
  const current = eventWindowRef.current;
288
- const next = boundBrowserSessionEventWindow([...current.events, ...batch]);
370
+ const next = appendBrowserSessionEventWindow(current, batch);
289
371
  const retained = {
290
372
  ...next,
291
373
  truncated: current.truncated || next.truncated,
@@ -293,7 +375,7 @@ export function useSessionEvents(
293
375
  eventWindowRef.current = retained;
294
376
  setEventWindow(retained);
295
377
  oldestSequenceRef.current = retained.events[0]?.sequence ?? null;
296
- newestSequenceRef.current = retained.events.at(-1)?.sequence ?? null;
378
+ newestSequenceRef.current = maxResumeSequenceOrNull(retained.events);
297
379
  if (retained.truncated) {
298
380
  hasOlderRef.current = true;
299
381
  setHasOlder(true);
@@ -306,6 +388,56 @@ export function useSessionEvents(
306
388
 
307
389
  void (async () => {
308
390
  try {
391
+ if (reconcileForegroundResume) {
392
+ setConnectionState("connecting");
393
+ const cursor = streamResumeSequenceRef.current;
394
+ let plan: ForegroundCatchupPlan = { kind: "resume" };
395
+ try {
396
+ plan = await planForegroundCatchup(client, workspaceId, sessionId, cursor, {
397
+ signal: controller.signal,
398
+ });
399
+ } catch {
400
+ if (controller.signal.aborted) return;
401
+ // This read is an optimization gate, not a new availability
402
+ // dependency. If it fails, preserve the SDK's exact cursor replay
403
+ // rather than blanking a usable timeline or failing the stream.
404
+ plan = { kind: "resume" };
405
+ }
406
+ if (!isCurrent()) return;
407
+ if (plan.kind === "append") {
408
+ const current = eventWindowRef.current;
409
+ assertAppendOrder(current.events, plan.events);
410
+ const status = observeSessionStatus(plan.events, sessionStatusRef);
411
+ const next = appendBrowserSessionEventWindow(current, plan.events);
412
+ const retained = {
413
+ ...next,
414
+ truncated: current.truncated || next.truncated,
415
+ };
416
+ eventWindowRef.current = retained;
417
+ oldestSequenceRef.current = retained.events[0]?.sequence ?? null;
418
+ newestSequenceRef.current = maxResumeSequenceOrNull(retained.events);
419
+ lastSequenceRef.current = Math.max(lastSequenceRef.current, plan.resumeSequence);
420
+ streamResumeSequenceRef.current = Math.max(
421
+ streamResumeSequenceRef.current,
422
+ plan.resumeSequence,
423
+ );
424
+ if (status !== undefined) {
425
+ setSessionStatusProjection(status);
426
+ }
427
+ setEventWindow(retained);
428
+ if (retained.truncated) {
429
+ hasOlderRef.current = true;
430
+ setHasOlder(true);
431
+ }
432
+ } else if (plan.kind === "reload") {
433
+ // A large/complex missed window would make the foreground timeline
434
+ // and pinned camera chase many rapid commits. Keep the last known
435
+ // complete window visible until the bounded latest replacement is
436
+ // ready, then install that replacement atomically below.
437
+ initialWindowLoadedRef.current = false;
438
+ setError(null);
439
+ }
440
+ }
309
441
  if (!fullReplay && !initialWindowLoadedRef.current) {
310
442
  setConnectionState("connecting");
311
443
  // First paint is ONE compact fetch — the newest window, revealed at
@@ -313,25 +445,42 @@ export function useSessionEvents(
313
445
  // reader actually scrolls up (the sentinel drives loadOlder).
314
446
  const window = await loadEventWindow(client, workspaceId, sessionId, {
315
447
  before: Number.MAX_SAFE_INTEGER,
316
- pageSize: INITIAL_TAIL_PAGE_SIZE,
448
+ pageSize: SESSION_HISTORY_PAGE_SIZE,
317
449
  targetGroups: Number.POSITIVE_INFINITY,
318
450
  maxFetches: INITIAL_FETCH_CAP,
451
+ boundaryPageCap: BOUNDARY_PAGE_CAP,
319
452
  signal: controller.signal,
320
453
  });
321
454
  if (!isCurrent()) {
322
455
  return;
323
456
  }
324
- observeSessionStatus(window.events, sessionStatusRef, setSessionStatusProjection);
457
+ const status = observeSessionStatus(window.events, sessionStatusRef);
325
458
  const retained = boundBrowserSessionEventWindow(window.events);
459
+ // A replacement tail retires requests against the discarded window.
460
+ // Ordinary SSE reconnects preserve navigation, but splicing an old
461
+ // page into this new tail could leave an inaccessible history gap.
462
+ navigationGenerationRef.current += 1;
463
+ loadingOlderRef.current = false;
464
+ loadingNewerRef.current = false;
465
+ loadingOldestRef.current = false;
466
+ setLoadingOlder(false);
467
+ setLoadingNewer(false);
468
+ setLoadingOldest(false);
326
469
  eventWindowRef.current = retained;
327
- setEventWindow(retained);
328
470
  oldestSequenceRef.current = retained.events[0]?.sequence ?? window.oldestSequence;
329
- newestSequenceRef.current = retained.events.at(-1)?.sequence ?? window.newestSequence;
471
+ newestSequenceRef.current =
472
+ maxResumeSequenceOrNull(retained.events) ?? window.newestSequence;
330
473
  hasOlderRef.current = window.hasOlder || retained.truncated;
331
474
  hasNewerRef.current = false;
332
475
  lastSequenceRef.current = window.newestSequence;
333
476
  streamResumeSequenceRef.current = window.newestSequence;
334
477
  initialWindowLoadedRef.current = true;
478
+ setInitialHistoryReady(true);
479
+ setError(null);
480
+ if (status !== undefined) {
481
+ setSessionStatusProjection(status);
482
+ }
483
+ setEventWindow(retained);
335
484
  setHasOlder(window.hasOlder || retained.truncated);
336
485
  setHasNewer(false);
337
486
  setInitialLoading(false);
@@ -350,7 +499,7 @@ export function useSessionEvents(
350
499
  .then(() => reconcileSession(sessionId))
351
500
  .catch((cause) => {
352
501
  if (isCurrent()) {
353
- setError(cause instanceof Error ? cause : new Error(String(cause)));
502
+ setError(normalizeError(cause));
354
503
  }
355
504
  });
356
505
  },
@@ -362,7 +511,7 @@ export function useSessionEvents(
362
511
  });
363
512
  for await (const event of stream) {
364
513
  if (!isCurrent()) break;
365
- const boundedEvent = boundBrowserLegacyEvent(event);
514
+ const boundedEvent = event;
366
515
  const boundedEventBytes = browserJsonBytes(boundedEvent);
367
516
  const separatorBytes = pending.length === 0 ? 0 : 1;
368
517
  if (
@@ -391,8 +540,12 @@ export function useSessionEvents(
391
540
  } catch (cause) {
392
541
  if (isCurrent()) {
393
542
  flush();
394
- setError(cause instanceof Error ? cause : new Error(String(cause)));
543
+ setError(normalizeError(cause));
395
544
  setConnectionState("error");
545
+ // A failed first compact-tail request has no later success path in
546
+ // this effect instance. End the loading gate so hosts can render the
547
+ // retryable error instead of an infinite initial spinner.
548
+ setInitialLoading(false);
396
549
  }
397
550
  }
398
551
  })();
@@ -414,6 +567,8 @@ export function useSessionEvents(
414
567
  workspaceId,
415
568
  sessionId,
416
569
  after,
570
+ enabled,
571
+ pageLive,
417
572
  streamEnabled,
418
573
  fullReplay,
419
574
  streamKey,
@@ -423,6 +578,7 @@ export function useSessionEvents(
423
578
  ]);
424
579
 
425
580
  const navigationBusy = (): boolean =>
581
+ loadingTargetRef.current ||
426
582
  loadingOlderRef.current ||
427
583
  loadingNewerRef.current ||
428
584
  loadingOldestRef.current ||
@@ -430,8 +586,8 @@ export function useSessionEvents(
430
586
 
431
587
  const loadOlder = useCallback(
432
588
  (): OlderHistoryLoadReceipt =>
433
- createOlderHistoryLoadReceipt(async (markCommitted) => {
434
- if (!sessionId || navigationBusy() || !hasOlderRef.current) {
589
+ createOlderHistoryLoadReceipt(async (markCommitted, preserveTail, markTailPreserved) => {
590
+ if (!ownsNavigation() || !sessionId || navigationBusy() || !hasOlderRef.current) {
435
591
  return false;
436
592
  }
437
593
  const before = oldestSequenceRef.current;
@@ -441,17 +597,20 @@ export function useSessionEvents(
441
597
  setHasOlder(false);
442
598
  return false;
443
599
  }
444
- const generation = generationRef.current;
600
+ const generation = navigationGenerationRef.current;
601
+ const isCurrent = () => ownsNavigation() && navigationGenerationRef.current === generation;
445
602
  loadingOlderRef.current = true;
446
603
  setLoadingOlder(true);
604
+ let published = false;
447
605
  try {
448
606
  const window = await loadEventWindow(client, workspaceId, sessionId, {
449
607
  before,
450
- pageSize: OLDER_PAGE_SIZE,
608
+ pageSize: SESSION_HISTORY_PAGE_SIZE,
451
609
  targetGroups: OLDER_GROUP_TARGET,
452
610
  maxFetches: OLDER_FETCH_CAP,
611
+ isCurrent,
453
612
  });
454
- if (generationRef.current !== generation) {
613
+ if (!isCurrent()) {
455
614
  return false;
456
615
  }
457
616
  if (window.events.length === 0) {
@@ -462,14 +621,16 @@ export function useSessionEvents(
462
621
  }
463
622
  const current = eventWindowRef.current;
464
623
  assertPrependOrder(current.events, window.events);
465
- // Freeze the live iterator before replacing its in-memory window. Rows
466
- // pending in the aborted iterator were never cursor-committed and will
467
- // be replayed from the retained high-water mark below.
468
- streamAbortRef.current?.abort();
469
- observeSessionStatus(window.events, sessionStatusRef, setSessionStatusProjection);
470
624
  const next = boundBrowserSessionEventWindow([...window.events, ...current.events], {
471
625
  direction: "oldest",
472
626
  });
627
+ if (preserveTail && maxResumeSequence(next.events) < maxResumeSequence(current.events)) {
628
+ // Automatic viewport filling must not navigate away from the
629
+ // reader's retained tail. Explicit history navigation may do so.
630
+ markTailPreserved();
631
+ return false;
632
+ }
633
+ const status = observeSessionStatus(window.events, sessionStatusRef);
473
634
  const retained = {
474
635
  ...next,
475
636
  truncated: current.truncated || next.truncated,
@@ -478,54 +639,99 @@ export function useSessionEvents(
478
639
  if (retainedOldest === null || retainedOldest >= before) {
479
640
  throw new Error("@opengeni/react: loadOlder made no durable sequence progress");
480
641
  }
481
- const retainedNewest = retained.events.at(-1)?.sequence ?? null;
642
+ const retainedNewest = maxResumeSequenceOrNull(retained.events);
643
+ const previousNewest = maxResumeSequenceOrNull(current.events);
644
+ const evictedLiveTail =
645
+ previousNewest !== null && retainedNewest !== null && retainedNewest < previousNewest;
646
+ if (evictedLiveTail && viewModeRef.current === "live") {
647
+ // The prepend leaves the live tip. Freeze the iterator before the
648
+ // window is replaced so a late flush cannot append live rows onto
649
+ // (and newest-bound away) the history the reader just requested.
650
+ streamAbortRef.current?.abort();
651
+ }
482
652
  eventWindowRef.current = retained;
483
653
  markCommitted();
484
- setEventWindow(retained);
485
654
  oldestSequenceRef.current = retainedOldest;
486
655
  newestSequenceRef.current = retainedNewest;
487
- streamResumeSequenceRef.current = maxResumeSequence(retained.events);
656
+ if (evictedLiveTail) {
657
+ streamResumeSequenceRef.current = maxResumeSequence(retained.events);
658
+ }
488
659
  // Oldest-directed eviction can discard newer in-memory rows. That fact
489
660
  // keeps windowTruncated true, but it does not imply older durable rows
490
661
  // exist; only the backward DB page can answer hasOlder truthfully.
491
662
  const olderStillAvailable = window.hasOlder;
492
663
  hasOlderRef.current = olderStillAvailable;
493
- setHasOlder(olderStillAvailable);
494
- if (viewModeRef.current === "history") {
664
+ let newer: boolean | null = null;
665
+ let enterHistory = false;
666
+ if (viewModeRef.current === "history" || evictedLiveTail) {
495
667
  const highWater = lastSequenceRef.current;
496
- const newer =
497
- retainedNewest !== null && (retainedNewest < highWater || retained.truncated);
668
+ newer = retainedNewest !== null && (retainedNewest < highWater || retained.truncated);
498
669
  hasNewerRef.current = newer;
499
- setHasNewer(newer);
500
- } else {
501
- // Live mode keeps the historical contract: reconnect and replay the
502
- // evicted tip through SSE so the window stays one contiguous suffix.
503
- setStreamEpoch((epoch) => epoch + 1);
670
+ if (evictedLiveTail) {
671
+ // A full oldest-directed browser window cannot also retain the
672
+ // live suffix. Keep the newly loaded history and page forward;
673
+ // reconnecting SSE here immediately newest-bounds the window and
674
+ // evicts every row the reader just requested.
675
+ viewModeRef.current = "history";
676
+ enterHistory = true;
677
+ }
504
678
  }
679
+ // Otherwise the merge is still one contiguous live suffix and the open
680
+ // SSE iterator keeps appending to it: every flush reads
681
+ // eventWindowRef, so rows delivered during or after this fetch land
682
+ // once, in order, on the prepended window. Reopening the stream here
683
+ // would flash a connecting state and re-run session reconciliation on
684
+ // every scroll-up for no data benefit.
685
+ loadingOlderRef.current = false;
686
+ startTransition(() => {
687
+ if (status !== undefined) {
688
+ setSessionStatusProjection(status);
689
+ }
690
+ setEventWindow(retained);
691
+ setHasOlder(olderStillAvailable);
692
+ if (newer !== null) {
693
+ setHasNewer(newer);
694
+ }
695
+ if (enterHistory) {
696
+ setViewMode("history");
697
+ }
698
+ setLoadingOlder(false);
699
+ });
700
+ published = true;
505
701
  return olderStillAvailable;
702
+ } catch (reason) {
703
+ if (!isCurrent()) return false;
704
+ throw reason;
506
705
  } finally {
507
- loadingOlderRef.current = false;
508
- setLoadingOlder(false);
706
+ if (!published && isCurrent()) {
707
+ loadingOlderRef.current = false;
708
+ setLoadingOlder(false);
709
+ }
509
710
  }
510
711
  }),
511
- [client, workspaceId, sessionId],
712
+ [client, workspaceId, sessionId, ownsNavigation],
512
713
  );
513
714
 
514
715
  const loadOldest = useCallback(async (): Promise<boolean> => {
515
- if (!sessionId || navigationBusy() || !hasOlderRef.current) {
716
+ if (!ownsNavigation() || !sessionId || navigationBusy() || !hasOlderRef.current) {
516
717
  return false;
517
718
  }
518
- const generation = generationRef.current;
719
+ // Explicit window replacement supersedes unresolved question destinations.
720
+ const generation = ++navigationGenerationRef.current;
721
+ const isCurrent = () => ownsNavigation() && navigationGenerationRef.current === generation;
722
+ setQuestionEvidence(null);
519
723
  loadingOldestRef.current = true;
520
724
  setLoadingOldest(true);
725
+ let published = false;
521
726
  try {
522
727
  const window = await loadForwardEventWindow(client, workspaceId, sessionId, {
523
728
  after: 0,
524
- pageSize: OLDEST_PAGE_SIZE,
729
+ pageSize: SESSION_HISTORY_PAGE_SIZE,
525
730
  targetGroups: OLDEST_GROUP_TARGET,
526
731
  maxFetches: OLDEST_FETCH_CAP,
732
+ isCurrent,
527
733
  });
528
- if (generationRef.current !== generation) {
734
+ if (!isCurrent()) {
529
735
  return false;
530
736
  }
531
737
  if (window.events.length === 0) {
@@ -534,17 +740,15 @@ export function useSessionEvents(
534
740
  return false;
535
741
  }
536
742
  streamAbortRef.current?.abort();
537
- observeSessionStatus(window.events, sessionStatusRef, setSessionStatusProjection);
743
+ const status = observeSessionStatus(window.events, sessionStatusRef);
538
744
  // Keep the oldest prefix — never the middle or tip.
539
745
  const retained = boundBrowserSessionEventWindow(window.events, { direction: "oldest" });
540
746
  const retainedOldest = retained.events[0]?.sequence ?? null;
541
- const retainedNewest = retained.events.at(-1)?.sequence ?? null;
747
+ const retainedNewest = maxResumeSequenceOrNull(retained.events);
542
748
  eventWindowRef.current = retained;
543
- setEventWindow(retained);
544
749
  oldestSequenceRef.current = retainedOldest;
545
750
  newestSequenceRef.current = retainedNewest;
546
751
  hasOlderRef.current = false;
547
- setHasOlder(false);
548
752
  const highWater = Math.max(lastSequenceRef.current, retainedNewest ?? 0);
549
753
  lastSequenceRef.current = highWater;
550
754
  const newer =
@@ -552,19 +756,37 @@ export function useSessionEvents(
552
756
  retained.truncated ||
553
757
  (retainedNewest !== null && retainedNewest < highWater);
554
758
  hasNewerRef.current = newer;
555
- setHasNewer(newer);
556
759
  initialWindowLoadedRef.current = true;
760
+ setInitialHistoryReady(true);
761
+ setError(null);
557
762
  viewModeRef.current = "history";
558
- setViewMode("history");
763
+ loadingOldestRef.current = false;
764
+ setNewerError(null);
765
+ startTransition(() => {
766
+ if (status !== undefined) {
767
+ setSessionStatusProjection(status);
768
+ }
769
+ setEventWindow(retained);
770
+ setHasOlder(false);
771
+ setHasNewer(newer);
772
+ setViewMode("history");
773
+ setLoadingOldest(false);
774
+ });
775
+ published = true;
559
776
  return newer;
777
+ } catch (reason) {
778
+ if (!isCurrent()) return false;
779
+ throw reason;
560
780
  } finally {
561
- loadingOldestRef.current = false;
562
- setLoadingOldest(false);
781
+ if (!published && isCurrent()) {
782
+ loadingOldestRef.current = false;
783
+ setLoadingOldest(false);
784
+ }
563
785
  }
564
- }, [client, workspaceId, sessionId]);
786
+ }, [client, workspaceId, sessionId, ownsNavigation]);
565
787
 
566
788
  const loadNewer = useCallback(async (): Promise<boolean> => {
567
- if (!sessionId || navigationBusy() || !hasNewerRef.current) {
789
+ if (!ownsNavigation() || !sessionId || navigationBusy() || !hasNewerRef.current) {
568
790
  return false;
569
791
  }
570
792
  const afterSequence = newestSequenceRef.current;
@@ -573,20 +795,24 @@ export function useSessionEvents(
573
795
  setHasNewer(false);
574
796
  return false;
575
797
  }
576
- const generation = generationRef.current;
798
+ const generation = navigationGenerationRef.current;
799
+ const isCurrent = () => ownsNavigation() && navigationGenerationRef.current === generation;
577
800
  loadingNewerRef.current = true;
578
801
  setLoadingNewer(true);
802
+ let published = false;
579
803
  try {
580
804
  const window = await loadForwardEventWindow(client, workspaceId, sessionId, {
581
805
  after: afterSequence,
582
- pageSize: NEWER_PAGE_SIZE,
806
+ pageSize: SESSION_HISTORY_PAGE_SIZE,
583
807
  targetGroups: NEWER_GROUP_TARGET,
584
808
  maxFetches: NEWER_FETCH_CAP,
809
+ isCurrent,
585
810
  });
586
- if (generationRef.current !== generation) {
811
+ if (!isCurrent()) {
587
812
  return false;
588
813
  }
589
814
  if (window.events.length === 0) {
815
+ setNewerError(null);
590
816
  hasNewerRef.current = false;
591
817
  setHasNewer(false);
592
818
  // Caught up with durable history — resume the live tip stream.
@@ -598,23 +824,20 @@ export function useSessionEvents(
598
824
  }
599
825
  const current = eventWindowRef.current;
600
826
  assertAppendOrder(current.events, window.events);
601
- observeSessionStatus(window.events, sessionStatusRef, setSessionStatusProjection);
827
+ const status = observeSessionStatus(window.events, sessionStatusRef);
602
828
  const previousOldest = current.events[0]?.sequence ?? null;
603
829
  // Moving forward through history: keep the newest suffix of the merge.
604
- const next = boundBrowserSessionEventWindow([...current.events, ...window.events], {
605
- direction: "newest",
606
- });
830
+ const next = appendBrowserSessionEventWindow(current, window.events);
607
831
  const retained = {
608
832
  ...next,
609
833
  truncated: current.truncated || next.truncated || window.hasNewer,
610
834
  };
611
835
  const retainedOldest = retained.events[0]?.sequence ?? null;
612
- const retainedNewest = retained.events.at(-1)?.sequence ?? null;
836
+ const retainedNewest = maxResumeSequenceOrNull(retained.events);
613
837
  if (retainedNewest === null || retainedNewest <= afterSequence) {
614
838
  throw new Error("@opengeni/react: loadNewer made no durable sequence progress");
615
839
  }
616
840
  eventWindowRef.current = retained;
617
- setEventWindow(retained);
618
841
  oldestSequenceRef.current = retainedOldest;
619
842
  newestSequenceRef.current = retainedNewest;
620
843
  // Evicting the start while paging forward re-arms hasOlder.
@@ -627,69 +850,261 @@ export function useSessionEvents(
627
850
  retainedOldest > 1))
628
851
  ) {
629
852
  hasOlderRef.current = true;
630
- setHasOlder(true);
631
853
  }
854
+ const rearmedOlder = hasOlderRef.current;
632
855
  const highWater = Math.max(lastSequenceRef.current, retainedNewest);
633
856
  lastSequenceRef.current = highWater;
634
857
  const newer = window.hasNewer || retainedNewest < highWater;
635
858
  hasNewerRef.current = newer;
636
- setHasNewer(newer);
859
+ let resumeLive = false;
637
860
  if (!newer) {
638
861
  streamResumeSequenceRef.current = retainedNewest;
639
862
  viewModeRef.current = "live";
640
- setViewMode("live");
641
- setStreamEpoch((epoch) => epoch + 1);
863
+ resumeLive = true;
642
864
  }
865
+ loadingNewerRef.current = false;
866
+ setNewerError(null);
867
+ startTransition(() => {
868
+ if (status !== undefined) {
869
+ setSessionStatusProjection(status);
870
+ }
871
+ setEventWindow(retained);
872
+ setHasOlder(rearmedOlder);
873
+ setHasNewer(newer);
874
+ if (resumeLive) {
875
+ setViewMode("live");
876
+ setStreamEpoch((epoch) => epoch + 1);
877
+ }
878
+ setLoadingNewer(false);
879
+ });
880
+ published = true;
643
881
  return newer;
882
+ } catch (reason) {
883
+ // A settled request from an old navigation lifetime is not a failure of
884
+ // the current session. Preserve current loading/error state as well.
885
+ if (!isCurrent()) {
886
+ return false;
887
+ }
888
+ setNewerError(normalizeError(reason));
889
+ // Keep authorization and integrity failures actionable for callers;
890
+ // timeline-owned invocations attach their own explicit recovery UI.
891
+ throw reason;
644
892
  } finally {
645
- loadingNewerRef.current = false;
646
- setLoadingNewer(false);
893
+ if (!published && isCurrent()) {
894
+ loadingNewerRef.current = false;
895
+ setLoadingNewer(false);
896
+ }
647
897
  }
648
- }, [client, workspaceId, sessionId]);
898
+ }, [client, workspaceId, sessionId, ownsNavigation]);
899
+
900
+ const jumpToSequence = useCallback(
901
+ async (sequence: number, navigationOptions?: { signal?: AbortSignal }): Promise<boolean> => {
902
+ const signal = navigationOptions?.signal;
903
+ if (!ownsNavigation() || !sessionId || !Number.isSafeInteger(sequence) || sequence < 1)
904
+ return false;
905
+ // A navigation that was already cancelled (for example Find closed before
906
+ // the caller could react) must not disturb any current view state.
907
+ if (signal?.aborted) return false;
908
+ // Explicit targets supersede both other targets and adjacent-page requests.
909
+ const generation = ++navigationGenerationRef.current;
910
+ setQuestionEvidence(null);
911
+ const current = () => generation === navigationGenerationRef.current && ownsNavigation();
912
+ const previousMode = viewModeRef.current;
913
+ streamAbortRef.current?.abort();
914
+ generationRef.current += 1;
915
+ viewModeRef.current = "history";
916
+ setViewMode("history");
917
+ loadingOlderRef.current = loadingNewerRef.current = loadingOldestRef.current = false;
918
+ setLoadingOlder(false);
919
+ setLoadingNewer(false);
920
+ setLoadingOldest(false);
921
+ loadingTargetRef.current = true;
922
+ setLoadingTarget(true);
923
+ let published = false;
924
+ try {
925
+ const [before, following] = await Promise.all([
926
+ loadPreviousPage(client, workspaceId, sessionId, sequence + 1, { pageSize: 128 }),
927
+ loadNextPage(client, workspaceId, sessionId, sequence, { pageSize: 128 }),
928
+ ]);
929
+ // Close-during-fetch fence: a pending jump must not replace the window
930
+ // after its owner (for example ConversationFind) has gone away. The
931
+ // finally block still settles loadingTarget for this generation.
932
+ if (signal?.aborted) return false;
933
+ if (!current()) return false;
934
+ if (!before.some((event) => event.sequence === sequence)) return false;
935
+ // Bound each side, keeping the target even if its message alone exceeds
936
+ // the ordinary byte budget. Never evict the very event being navigated to.
937
+ const prefix = boundBrowserSessionEventWindow(before, {
938
+ direction: "newest",
939
+ maxBytes: SESSION_EVENT_BROWSER_MAX_BYTES / 2,
940
+ });
941
+ const retained = boundBrowserSessionEventWindow([...prefix.events, ...following], {
942
+ direction: "oldest",
943
+ });
944
+ if (!retained.events.some((event) => event.sequence === sequence)) return false;
945
+ streamAbortRef.current?.abort();
946
+ generationRef.current += 1;
947
+ const status = observeSessionStatus(retained.events, sessionStatusRef);
948
+ eventWindowRef.current = retained;
949
+ oldestSequenceRef.current = retained.events[0]!.sequence;
950
+ newestSequenceRef.current = maxResumeSequenceOrNull(retained.events);
951
+ lastSequenceRef.current = Math.max(lastSequenceRef.current, newestSequenceRef.current ?? 0);
952
+ hasOlderRef.current = !isLogStart(retained.events[0]!);
953
+ // Compact/byte-bounded pages may be short despite more durable history.
954
+ // Only an empty forward page proves the end (same rule as loadNewer).
955
+ hasNewerRef.current =
956
+ following.length > 0 ||
957
+ retained.truncated ||
958
+ (newestSequenceRef.current ?? 0) < lastSequenceRef.current;
959
+ initialWindowLoadedRef.current = true;
960
+ setInitialHistoryReady(true);
961
+ setError(null);
962
+ viewModeRef.current = "history";
963
+ setEventWindow(retained);
964
+ setHasOlder(hasOlderRef.current);
965
+ setHasNewer(hasNewerRef.current);
966
+ setInitialLoading(false);
967
+ setViewMode("history");
968
+ setNewerError(null);
969
+ if (status !== undefined) setSessionStatusProjection(status);
970
+ published = true;
971
+ return true;
972
+ } catch (reason) {
973
+ if (!current() || signal?.aborted) return false;
974
+ throw reason;
975
+ } finally {
976
+ if (current()) {
977
+ loadingTargetRef.current = false;
978
+ setLoadingTarget(false);
979
+ if (!published && previousMode === "live") {
980
+ viewModeRef.current = "live";
981
+ setViewMode("live");
982
+ setStreamEpoch((epoch) => epoch + 1);
983
+ }
984
+ }
985
+ }
986
+ },
987
+ [client, workspaceId, sessionId, ownsNavigation],
988
+ );
989
+
990
+ const jumpToLatestQuestion = useCallback(
991
+ async (questionOptions?: LatestQuestionOptions): Promise<number | null> => {
992
+ if (!sessionId || !ownsNavigation()) return null;
993
+ let generation = navigationGenerationRef.current;
994
+ const current = () => ownsNavigation() && generation === navigationGenerationRef.current;
995
+ try {
996
+ // Optional action code stays off the session-open path. Capture identity
997
+ // before the import and fence its completion just like network reads.
998
+ const { resolveLatestQuestion } = await import("./latest-question");
999
+ if (!current()) return null;
1000
+ const destination = await resolveLatestQuestion({
1001
+ client,
1002
+ workspaceId,
1003
+ sessionId,
1004
+ isCurrent: current,
1005
+ resumeSequence: maxResumeSequence,
1006
+ options: questionOptions,
1007
+ });
1008
+ if (!destination || !current()) return null;
1009
+ if (!eventWindowRef.current.events.some((event) => event.sequence === destination.anchor)) {
1010
+ const navigation = jumpToSequence(destination.anchor);
1011
+ generation = navigationGenerationRef.current;
1012
+ if (!(await navigation) || !current()) return null;
1013
+ }
1014
+ setQuestionEvidence({
1015
+ client,
1016
+ streamKey,
1017
+ anchor: destination.anchor,
1018
+ events: destination.events,
1019
+ });
1020
+ return destination.questionSequence;
1021
+ } catch (reason) {
1022
+ if (!current()) return null;
1023
+ throw reason;
1024
+ }
1025
+ },
1026
+ [client, workspaceId, sessionId, streamKey, ownsNavigation, jumpToSequence],
1027
+ );
649
1028
 
650
1029
  const jumpToLatest = useCallback(async (): Promise<void> => {
1030
+ // An old host retry closure must not clear a replacement session's error
1031
+ // or abort its live feed before the passive effect cleanup has run.
1032
+ if (!ownsNavigation()) return;
1033
+ if (loadingTargetRef.current) {
1034
+ navigationGenerationRef.current += 1;
1035
+ loadingTargetRef.current = false;
1036
+ setLoadingTarget(false);
1037
+ }
651
1038
  if (!sessionId || navigationBusy()) {
652
1039
  return;
653
1040
  }
1041
+ navigationGenerationRef.current += 1;
1042
+ setQuestionEvidence(null);
654
1043
  loadingLatestRef.current = true;
655
1044
  setLoadingLatest(true);
1045
+ setNewerError(null);
1046
+ setError(null);
1047
+ let published = false;
656
1048
  try {
657
1049
  streamAbortRef.current?.abort();
658
1050
  hasNewerRef.current = false;
659
- setHasNewer(false);
660
1051
  hasOlderRef.current = false;
661
- setHasOlder(false);
662
1052
  newestSequenceRef.current = null;
663
1053
  oldestSequenceRef.current = null;
664
1054
  eventWindowRef.current = EMPTY_EVENT_WINDOW;
665
- setEventWindow(EMPTY_EVENT_WINDOW);
666
1055
  initialWindowLoadedRef.current = false;
667
1056
  streamResumeSequenceRef.current = after;
668
- setInitialLoading(true);
669
1057
  viewModeRef.current = "live";
670
- setViewMode("live");
671
- setStreamEpoch((epoch) => epoch + 1);
1058
+ loadingLatestRef.current = false;
1059
+ startTransition(() => {
1060
+ setHasNewer(false);
1061
+ setHasOlder(false);
1062
+ setEventWindow(EMPTY_EVENT_WINDOW);
1063
+ setInitialLoading(true);
1064
+ setViewMode("live");
1065
+ setStreamEpoch((epoch) => epoch + 1);
1066
+ setLoadingLatest(false);
1067
+ });
1068
+ published = true;
672
1069
  // Effect reloads the tip. The tip fetch itself is owned by the effect.
673
1070
  await Promise.resolve();
674
1071
  } finally {
675
- loadingLatestRef.current = false;
676
- setLoadingLatest(false);
1072
+ if (!published) {
1073
+ loadingLatestRef.current = false;
1074
+ setLoadingLatest(false);
1075
+ }
677
1076
  }
678
- }, [after, sessionId]);
1077
+ }, [after, sessionId, ownsNavigation]);
679
1078
 
680
1079
  const identityMatches = stateStreamKey === streamKey;
681
1080
  const visibleEvents = identityMatches ? eventWindow.events : EMPTY_EVENTS;
682
- const timeline = useMemo(() => buildTimeline(visibleEvents), [visibleEvents]);
1081
+ const timeline = useMemo(() => {
1082
+ const witness =
1083
+ questionEvidence?.client === client &&
1084
+ questionEvidence.streamKey === streamKey &&
1085
+ visibleEvents.some((event) => event.sequence === questionEvidence.anchor)
1086
+ ? questionEvidence.events
1087
+ : EMPTY_EVENTS;
1088
+ const timelineOptions = { partialStart: hasOlder || eventWindow.truncated || after > 0 };
1089
+ if (witness.length === 0) return buildTimeline(visibleEvents, timelineOptions);
1090
+ const ids = new Set(visibleEvents.map((event) => event.id));
1091
+ return buildTimeline(
1092
+ [...visibleEvents, ...witness.filter((event) => !ids.has(event.id))],
1093
+ timelineOptions,
1094
+ );
1095
+ }, [visibleEvents, hasOlder, eventWindow.truncated, after, questionEvidence, client, streamKey]);
683
1096
 
684
1097
  return {
685
1098
  events: visibleEvents,
686
1099
  timeline,
687
1100
  sessionStatus: identityMatches ? sessionStatusProjection : null,
1101
+ sessionStatusSequence: identityMatches ? sessionStatusRef.current.sequence : 0,
688
1102
  connectionState: identityMatches ? connectionState : "idle",
689
1103
  lastSequence: identityMatches ? lastSequenceRef.current : after,
690
1104
  windowBytes: identityMatches ? eventWindow.bytes : 2,
691
1105
  windowTruncated: identityMatches ? eventWindow.truncated : false,
692
1106
  initialLoading: fullReplay ? false : identityMatches ? initialLoading : true,
1107
+ initialHistoryReady: identityMatches && initialHistoryReady,
693
1108
  hasOlder: !identityMatches ? false : hasOlder,
694
1109
  loadingOlder: !identityMatches ? false : loadingOlder,
695
1110
  loadOlder,
@@ -700,7 +1115,10 @@ export function useSessionEvents(
700
1115
  loadOldest,
701
1116
  loadingLatest: !identityMatches ? false : loadingLatest,
702
1117
  jumpToLatest,
703
- error: identityMatches ? error : null,
1118
+ jumpToLatestQuestion,
1119
+ jumpToSequence,
1120
+ loadingTarget: identityMatches && loadingTarget,
1121
+ error: identityMatches ? (newerError ?? error) : null,
704
1122
  };
705
1123
  }
706
1124
 
@@ -709,9 +1127,10 @@ export function useSessionEvents(
709
1127
  * accumulation retains the newest suffix; backward paging retains the oldest
710
1128
  * prefix so newly fetched history cannot be immediately evicted. This is
711
1129
  * deliberately separate from durable history and transport paging: when a
712
- * backward page evicts the live tail, the hook reconnects from the retained
713
- * high-water mark while preserving the highest-ever-observed sequence
714
- * separately. The source event remains durable in PostgreSQL throughout.
1130
+ * backward page evicts the live tail, the hook enters bounded history mode and
1131
+ * preserves the highest-ever-observed sequence separately for forward paging.
1132
+ * The source event remains durable in PostgreSQL throughout. An event larger
1133
+ * than the byte target is retained alone, never replaced by a lossy preview.
715
1134
  */
716
1135
  export function boundBrowserSessionEventWindow(
717
1136
  events: readonly SessionEvent[],
@@ -723,7 +1142,7 @@ export function boundBrowserSessionEventWindow(
723
1142
  ): BrowserSessionEventWindow {
724
1143
  const maxBytes = Math.max(1024, options.maxBytes ?? SESSION_EVENT_BROWSER_MAX_BYTES);
725
1144
  const maxCount = Math.max(1, Math.floor(options.maxCount ?? SESSION_EVENT_BROWSER_MAX_COUNT));
726
- const safe = events.map(boundBrowserLegacyEvent);
1145
+ const safe = events;
727
1146
  const selected: SessionEvent[] = [];
728
1147
  let bytes = 2; // []
729
1148
  const direction = options.direction ?? "newest";
@@ -734,7 +1153,7 @@ export function boundBrowserSessionEventWindow(
734
1153
  const event = safe[index]!;
735
1154
  const eventBytes = browserJsonBytes(event);
736
1155
  const separator = selected.length === 0 ? 0 : 1;
737
- if (bytes + separator + eventBytes > maxBytes) break;
1156
+ if (selected.length > 0 && bytes + separator + eventBytes > maxBytes) break;
738
1157
  selected.push(event);
739
1158
  bytes += separator + eventBytes;
740
1159
  }
@@ -746,255 +1165,41 @@ export function boundBrowserSessionEventWindow(
746
1165
  };
747
1166
  }
748
1167
 
749
- function boundBrowserLegacyEvent(event: SessionEvent): SessionEvent {
750
- // The server-side canonical projection lives in @opengeni/contracts. The
751
- // publishable React package may depend only on the zero-runtime-dependency
752
- // SDK, so this is intentionally a last-resort client guard rather than a
753
- // second durable representation. Reconstructing the SDK wire shape prevents
754
- // legacy/malformed extra properties from bypassing the browser byte cap.
755
- const serialized = browserSerialize(event);
756
- const originalBytes = serialized.serializable
757
- ? encoder.encode(serialized.value).byteLength
758
- : null;
759
- const typeIsSafe =
760
- browserUtf8Bytes(event.type) <= BROWSER_EVENT_TYPE_MAX_BYTES &&
761
- !event.type.includes("\n") &&
762
- !event.type.includes("\r");
763
- const clientEventId = boundBrowserOptionalText(
764
- event.clientEventId,
765
- BROWSER_EVENT_CLIENT_ID_MAX_BYTES,
766
- );
767
- const duplicateReason = boundBrowserOptionalText(
768
- event.duplicateReason,
769
- BROWSER_EVENT_DUPLICATE_REASON_MAX_BYTES,
770
- );
771
-
772
- if (
773
- serialized.serializable &&
774
- originalBytes !== null &&
775
- originalBytes <= SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES &&
776
- typeIsSafe &&
777
- clientEventId === event.clientEventId &&
778
- duplicateReason === event.duplicateReason
779
- ) {
780
- return event;
781
- }
782
-
783
- const envelopeProjection = [
784
- !typeIsSafe
785
- ? browserEnvelopeFieldProjection("type", event.type, "session.event.envelope_omitted")
786
- : null,
787
- clientEventId !== event.clientEventId
788
- ? browserEnvelopeFieldProjection("clientEventId", event.clientEventId, clientEventId)
789
- : null,
790
- duplicateReason !== event.duplicateReason
791
- ? browserEnvelopeFieldProjection("duplicateReason", event.duplicateReason, duplicateReason)
792
- : null,
793
- ].filter((field) => field !== null);
794
- const payloadSerialization = browserSerialize(event.payload);
795
- const payloadBytes = payloadSerialization.serializable
796
- ? encoder.encode(payloadSerialization.value).byteLength
797
- : null;
798
- const preview = truncateBrowserUtf8Middle(
799
- payloadSerialization.value,
800
- BROWSER_EVENT_PAYLOAD_PREVIEW_MAX_BYTES,
801
- );
802
- const truncation = {
803
- truncated: true as const,
804
- surface: "browser_legacy_guard" as const,
805
- reason: serialized.serializable ? "event_envelope_bytes_exceeded" : "event_not_serializable",
806
- originalBytes,
807
- deliveredBytes: 0,
808
- omittedBytes: originalBytes,
809
- estimatedOriginalTokens: originalBytes === null ? null : Math.ceil(originalBytes / 4),
810
- estimatedDeliveredTokens: 0,
811
- fullEvidence: { available: false as const, reason: "not_retained" as const },
812
- details: [
813
- {
814
- path: "$.payload",
815
- kind: "payload_preview",
816
- originalBytes: payloadBytes,
817
- deliveredBytes: browserUtf8Bytes(preview),
818
- },
819
- ],
820
- };
821
- const payload: Record<string, unknown> = {
822
- ...browserPayloadIdentity(event.payload),
823
- preview,
824
- ...(envelopeProjection.length > 0
825
- ? {
826
- originalType: boundBrowserText(event.type, BROWSER_EVENT_TYPE_MAX_BYTES),
827
- envelopeProjection: {
828
- truncated: true,
829
- surface: "browser_legacy_guard",
830
- fields: envelopeProjection,
831
- },
832
- }
833
- : {}),
834
- truncation,
835
- };
836
- const bounded: SessionEvent = {
837
- id: boundBrowserText(event.id, BROWSER_EVENT_ID_MAX_BYTES),
838
- workspaceId: boundBrowserText(event.workspaceId, BROWSER_EVENT_ID_MAX_BYTES),
839
- sessionId: boundBrowserText(event.sessionId, BROWSER_EVENT_ID_MAX_BYTES),
840
- sequence: event.sequence,
841
- type: typeIsSafe ? event.type : "session.event.envelope_omitted",
842
- payload,
843
- occurredAt: boundBrowserText(event.occurredAt, BROWSER_EVENT_ID_MAX_BYTES),
844
- clientEventId,
845
- turnId: boundBrowserOptionalText(event.turnId, BROWSER_EVENT_ID_MAX_BYTES),
846
- turnGeneration: event.turnGeneration,
847
- turnAttemptId: boundBrowserOptionalText(event.turnAttemptId, BROWSER_EVENT_ID_MAX_BYTES),
848
- turnAssociation: event.turnAssociation,
849
- duplicateOfEventId: boundBrowserOptionalText(
850
- event.duplicateOfEventId,
851
- BROWSER_EVENT_ID_MAX_BYTES,
852
- ),
853
- duplicateReason,
854
- };
855
-
856
- settleBrowserEventTruncation(bounded, truncation);
857
- if (browserJsonBytes(bounded) > SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES) {
858
- payload.preview = truncateBrowserUtf8Middle(String(payload.preview), 4 * 1024);
859
- truncation.details = truncation.details.slice(0, 1);
860
- settleBrowserEventTruncation(bounded, truncation);
861
- }
862
- if (browserJsonBytes(bounded) > SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES) {
863
- bounded.clientEventId = null;
864
- bounded.duplicateReason = null;
865
- payload.preview = "[legacy event omitted at the browser byte boundary]";
866
- settleBrowserEventTruncation(bounded, truncation);
867
- }
868
- return bounded;
869
- }
870
-
871
- function browserJsonBytes(value: unknown): number {
872
- return encoder.encode(browserSerialize(value).value).byteLength;
873
- }
874
-
875
- function browserSerialize(value: unknown): { value: string; serializable: boolean } {
876
- try {
877
- const serialized = JSON.stringify(value);
878
- return { value: serialized === undefined ? "null" : serialized, serializable: true };
879
- } catch {
880
- return {
881
- value: '"[unserializable event payload omitted at browser boundary]"',
882
- serializable: false,
883
- };
1168
+ /** @internal Append immutable events without serializing the retained history again. */
1169
+ export function appendBrowserSessionEventWindow(
1170
+ current: BrowserSessionEventWindow,
1171
+ batch: readonly SessionEvent[],
1172
+ options: { maxBytes?: number; maxCount?: number } = {},
1173
+ ): BrowserSessionEventWindow {
1174
+ const maxBytes = Math.max(1024, options.maxBytes ?? SESSION_EVENT_BROWSER_MAX_BYTES);
1175
+ const maxCount = Math.max(1, Math.floor(options.maxCount ?? SESSION_EVENT_BROWSER_MAX_COUNT));
1176
+ const events = [...current.events, ...batch];
1177
+ let bytes = current.bytes;
1178
+ for (let index = 0; index < batch.length; index += 1) {
1179
+ bytes += browserJsonBytes(batch[index]) + (current.events.length + index > 0 ? 1 : 0);
884
1180
  }
885
- }
886
-
887
- function browserPayloadIdentity(payload: unknown): Record<string, unknown> {
888
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) return {};
889
- const record = payload as Record<string, unknown>;
890
- const identity: Record<string, unknown> = {};
891
- for (const field of BROWSER_EVENT_PAYLOAD_IDENTITY_FIELDS) {
892
- const value = record[field];
893
- if (typeof value === "string") {
894
- identity[field] = boundBrowserText(value, BROWSER_EVENT_ID_MAX_BYTES);
895
- } else if (typeof value === "number" || typeof value === "boolean" || value === null) {
896
- identity[field] = value;
897
- }
1181
+ let start = 0;
1182
+ // Preserve the newest event even when it alone exceeds the byte target,
1183
+ // matching the full-window reducer and retaining exact cursor progress.
1184
+ while (events.length - start > 1 && (bytes > maxBytes || events.length - start > maxCount)) {
1185
+ bytes -= browserJsonBytes(events[start]) + 1;
1186
+ start += 1;
898
1187
  }
899
- return identity;
900
- }
901
-
902
- function browserEnvelopeFieldProjection(
903
- field: string,
904
- original: string | null | undefined,
905
- delivered: string | null | undefined,
906
- ): { field: string; originalBytes: number; deliveredBytes: number } {
907
1188
  return {
908
- field,
909
- originalBytes: typeof original === "string" ? browserUtf8Bytes(original) : 0,
910
- deliveredBytes: typeof delivered === "string" ? browserUtf8Bytes(delivered) : 0,
1189
+ events: start === 0 ? events : events.slice(start),
1190
+ bytes,
1191
+ truncated: current.truncated || start > 0,
911
1192
  };
912
1193
  }
913
1194
 
914
- function boundBrowserOptionalText<T extends string | null | undefined>(
915
- value: T,
916
- maxBytes: number,
917
- ): T {
918
- return (typeof value === "string" ? boundBrowserText(value, maxBytes) : value) as T;
919
- }
920
-
921
- function boundBrowserText(value: string, maxBytes: number): string {
922
- const bytes = encoder.encode(value);
923
- if (bytes.byteLength <= maxBytes) return value;
924
- const marker = "…[truncated]";
925
- const prefixBudget = Math.max(0, maxBytes - browserUtf8Bytes(marker));
926
- let prefixEnd = Math.min(prefixBudget, bytes.byteLength);
927
- while (
928
- prefixEnd > 0 &&
929
- prefixEnd < bytes.byteLength &&
930
- isBrowserUtf8Continuation(bytes[prefixEnd]!)
931
- ) {
932
- prefixEnd -= 1;
933
- }
934
- return `${decoder.decode(bytes.subarray(0, prefixEnd))}${marker}`;
935
- }
936
-
937
- function truncateBrowserUtf8Middle(value: string, maxBytes: number): string {
938
- const bytes = encoder.encode(value);
939
- if (bytes.byteLength <= maxBytes) return value;
940
- const marker = `…[${bytes.byteLength - maxBytes} bytes omitted]…`;
941
- const contentBudget = Math.max(0, maxBytes - browserUtf8Bytes(marker));
942
- const leftBudget = Math.floor(contentBudget / 2);
943
- const rightBudget = contentBudget - leftBudget;
944
- let leftEnd = Math.min(leftBudget, bytes.byteLength);
945
- while (leftEnd > 0 && leftEnd < bytes.byteLength && isBrowserUtf8Continuation(bytes[leftEnd]!)) {
946
- leftEnd -= 1;
947
- }
948
- let rightStart = Math.max(0, bytes.byteLength - rightBudget);
949
- while (rightStart < bytes.byteLength && isBrowserUtf8Continuation(bytes[rightStart]!)) {
950
- rightStart += 1;
951
- }
952
- return `${decoder.decode(bytes.subarray(0, leftEnd))}${marker}${decoder.decode(bytes.subarray(rightStart))}`;
953
- }
954
-
955
- function settleBrowserEventTruncation(
956
- event: SessionEvent,
957
- truncation: {
958
- originalBytes: number | null;
959
- deliveredBytes: number;
960
- omittedBytes: number | null;
961
- estimatedDeliveredTokens: number;
962
- },
963
- ): void {
964
- for (let attempt = 0; attempt < 16; attempt += 1) {
965
- const deliveredBytes = browserJsonBytes(event);
966
- const omittedBytes =
967
- truncation.originalBytes === null
968
- ? null
969
- : Math.max(0, truncation.originalBytes - deliveredBytes);
970
- const estimatedDeliveredTokens = Math.ceil(deliveredBytes / 4);
971
- if (
972
- truncation.deliveredBytes === deliveredBytes &&
973
- truncation.omittedBytes === omittedBytes &&
974
- truncation.estimatedDeliveredTokens === estimatedDeliveredTokens
975
- ) {
976
- return;
977
- }
978
- truncation.deliveredBytes = deliveredBytes;
979
- truncation.omittedBytes = omittedBytes;
980
- truncation.estimatedDeliveredTokens = estimatedDeliveredTokens;
981
- }
982
- throw new RangeError("Browser event byte accounting did not converge");
983
- }
984
-
985
- function browserUtf8Bytes(value: string): number {
986
- return encoder.encode(value).byteLength;
987
- }
988
-
989
- function isBrowserUtf8Continuation(value: number): boolean {
990
- return (value & 0xc0) === 0x80;
1195
+ function browserJsonBytes(value: unknown): number {
1196
+ return encoder.encode(JSON.stringify(value)).byteLength;
991
1197
  }
992
1198
 
993
1199
  function observeSessionStatus(
994
1200
  events: readonly SessionEvent[],
995
1201
  ref: { current: { sequence: number; status: SessionStatus | null } },
996
- setStatus: (status: SessionStatus | null) => void,
997
- ): void {
1202
+ ): SessionStatus | undefined {
998
1203
  let latest: { sequence: number; status: SessionStatus } | null = null;
999
1204
  for (const event of events) {
1000
1205
  const status = sessionStatusFromEvents([event]);
@@ -1002,9 +1207,71 @@ function observeSessionStatus(
1002
1207
  latest = { sequence: event.sequence, status };
1003
1208
  }
1004
1209
  }
1005
- if (!latest || latest.sequence < ref.current.sequence) return;
1210
+ if (!latest || latest.sequence < ref.current.sequence) return undefined;
1006
1211
  ref.current = latest;
1007
- setStatus(latest.status);
1212
+ return latest.status;
1213
+ }
1214
+
1215
+ type ForegroundCatchupPlan =
1216
+ | { kind: "resume" }
1217
+ | { kind: "append"; events: SessionEvent[]; resumeSequence: number }
1218
+ | { kind: "reload" };
1219
+
1220
+ /**
1221
+ * Decide how a sustained hidden-tab suspension rejoins the durable event log.
1222
+ *
1223
+ * `lastSequence - cursor` is the exact raw durable work missed, but it is not
1224
+ * the number of visible messages: one streaming answer can own thousands of
1225
+ * adjacent delta rows. Small raw gaps therefore resume directly; medium gaps
1226
+ * get one forward compact page and are judged by the browser-visible result;
1227
+ * a page that cannot prove complete bounded coverage, or that would add too
1228
+ * many rows/groups/bytes, reloads the latest tail instead.
1229
+ */
1230
+ async function planForegroundCatchup(
1231
+ client: EmbeddedSessionClientLike,
1232
+ workspaceId: string,
1233
+ sessionId: string,
1234
+ cursor: number,
1235
+ options: { signal?: AbortSignal } = {},
1236
+ ): Promise<ForegroundCatchupPlan> {
1237
+ if (options.signal?.aborted) throw abortError();
1238
+ const session = await client.getSession(workspaceId, sessionId, {
1239
+ fresh: true,
1240
+ ...(options.signal ? { signal: options.signal } : {}),
1241
+ });
1242
+ if (options.signal?.aborted) throw abortError();
1243
+ const durableHead = Math.max(cursor, session.lastSequence);
1244
+ const rawGap = durableHead - cursor;
1245
+ if (rawGap <= FOREGROUND_DIRECT_REPLAY_MAX_SEQUENCES) {
1246
+ return { kind: "resume" };
1247
+ }
1248
+ if (rawGap > FOREGROUND_COMPACT_PROBE_MAX_SEQUENCES) {
1249
+ return { kind: "reload" };
1250
+ }
1251
+
1252
+ const page = await loadNextPage(client, workspaceId, sessionId, cursor, {
1253
+ pageSize: rawGap,
1254
+ ...(options.signal ? { signal: options.signal } : {}),
1255
+ });
1256
+ const events = page.filter((event) => eventResumeSequence(event) > cursor);
1257
+ if (events.length === 0) {
1258
+ return { kind: "reload" };
1259
+ }
1260
+ assertAscending(events);
1261
+ const resumeSequence = maxResumeSequence(events);
1262
+ if (resumeSequence < durableHead) {
1263
+ // Byte/count truncation or an incomplete projection means this page cannot
1264
+ // prove that one append reaches the head observed above.
1265
+ return { kind: "reload" };
1266
+ }
1267
+ if (
1268
+ events.length > FOREGROUND_COMPACT_CATCHUP_MAX_EVENTS ||
1269
+ groupCount(events) > FOREGROUND_COMPACT_CATCHUP_MAX_GROUPS ||
1270
+ browserJsonBytes(events) > FOREGROUND_COMPACT_CATCHUP_MAX_BYTES
1271
+ ) {
1272
+ return { kind: "reload" };
1273
+ }
1274
+ return { kind: "append", events, resumeSequence };
1008
1275
  }
1009
1276
 
1010
1277
  type LoadedEventWindow = {
@@ -1030,7 +1297,9 @@ async function loadEventWindow(
1030
1297
  pageSize: number;
1031
1298
  targetGroups: number;
1032
1299
  maxFetches: number;
1300
+ boundaryPageCap?: number;
1033
1301
  signal?: AbortSignal;
1302
+ isCurrent?: () => boolean;
1034
1303
  },
1035
1304
  ): Promise<LoadedEventWindow> {
1036
1305
  let cursor = options.before;
@@ -1046,6 +1315,7 @@ async function loadEventWindow(
1046
1315
  pageSize: options.pageSize,
1047
1316
  ...(options.signal ? { signal: options.signal } : {}),
1048
1317
  });
1318
+ if (options.isCurrent?.() === false) throw abortError();
1049
1319
  fetches += 1;
1050
1320
  if (page.length === 0) {
1051
1321
  reachedStart = true;
@@ -1064,19 +1334,19 @@ async function loadEventWindow(
1064
1334
  // turn boundary already in the buffer — the dropped fragment is refetched by
1065
1335
  // the next loadOlder (everything below the new oldest sequence), whose own
1066
1336
  // window snaps the same way, so every seam lands on a turn start. Extra
1067
- // pages are fetched only when the buffer holds no boundary at all (one
1068
- // monster turn); past the cap a mid-turn top is accepted.
1337
+ // page is fetched only when the buffer holds no boundary at all (one dense
1338
+ // turn); past the cap the existing truncation/hasOlder signal remains true.
1069
1339
  let snapPages = 0;
1070
1340
  while (
1071
1341
  !reachedStart &&
1072
1342
  findBoundaryIndex(buffer) === -1 &&
1073
- snapPages < BOUNDARY_PAGE_CAP &&
1074
- fetches < options.maxFetches
1343
+ snapPages < (options.boundaryPageCap ?? 0)
1075
1344
  ) {
1076
1345
  const page = await loadPreviousPage(client, workspaceId, sessionId, cursor, {
1077
1346
  pageSize: options.pageSize,
1078
1347
  ...(options.signal ? { signal: options.signal } : {}),
1079
1348
  });
1349
+ if (options.isCurrent?.() === false) throw abortError();
1080
1350
  fetches += 1;
1081
1351
  snapPages += 1;
1082
1352
  if (page.length === 0) {
@@ -1097,6 +1367,8 @@ async function loadEventWindow(
1097
1367
  buffer = buffer.slice(boundary);
1098
1368
  }
1099
1369
  }
1370
+ const densityWindow = trimBackwardToGroupTarget(buffer, options.targetGroups);
1371
+ buffer = densityWindow.events;
1100
1372
 
1101
1373
  const oldest = buffer[0] ?? null;
1102
1374
  const newest = buffer[buffer.length - 1] ?? null;
@@ -1104,21 +1376,26 @@ async function loadEventWindow(
1104
1376
  events: buffer,
1105
1377
  oldestSequence: oldest?.sequence ?? null,
1106
1378
  newestSequence: newest ? maxResumeSequence(buffer) : 0,
1107
- hasOlder: buffer.length > 0 && !reachedStart && oldest?.type !== "session.created",
1379
+ hasOlder:
1380
+ buffer.length > 0 &&
1381
+ (densityWindow.trimmed || (!reachedStart && oldest?.type !== "session.created")),
1108
1382
  };
1109
1383
  }
1110
1384
 
1111
1385
  /** Index of the oldest clean turn start in the buffer, or -1. */
1112
1386
  function findBoundaryIndex(events: SessionEvent[]): number {
1113
1387
  for (let index = 0; index < events.length; index += 1) {
1114
- const type = events[index]!.type;
1115
- if (type === "session.created" || type === "user.message") {
1388
+ if (isTurnBoundary(events[index]!)) {
1116
1389
  return index;
1117
1390
  }
1118
1391
  }
1119
1392
  return -1;
1120
1393
  }
1121
1394
 
1395
+ function isTurnBoundary(event: SessionEvent): boolean {
1396
+ return event.type === "session.created" || event.type === "user.message";
1397
+ }
1398
+
1122
1399
  /**
1123
1400
  * Forward (oldest→newer) density-bounded window. Used for jump-to-start and
1124
1401
  * loadNewer so the browser never walks the whole middle gap.
@@ -1133,6 +1410,7 @@ async function loadForwardEventWindow(
1133
1410
  targetGroups: number;
1134
1411
  maxFetches: number;
1135
1412
  signal?: AbortSignal;
1413
+ isCurrent?: () => boolean;
1136
1414
  },
1137
1415
  ): Promise<LoadedForwardEventWindow> {
1138
1416
  let cursor = options.after;
@@ -1148,6 +1426,7 @@ async function loadForwardEventWindow(
1148
1426
  pageSize: options.pageSize,
1149
1427
  ...(options.signal ? { signal: options.signal } : {}),
1150
1428
  });
1429
+ if (options.isCurrent?.() === false) throw abortError();
1151
1430
  fetches += 1;
1152
1431
  if (page.length === 0) {
1153
1432
  reachedEnd = true;
@@ -1155,12 +1434,15 @@ async function loadForwardEventWindow(
1155
1434
  }
1156
1435
  assertAscending(page);
1157
1436
  buffer = [...buffer, ...page];
1158
- cursor = page[page.length - 1]!.sequence;
1159
- if (page.length < options.pageSize) {
1160
- reachedEnd = true;
1161
- break;
1162
- }
1437
+ // Compact events retain their first raw sequence in `sequence` and expose
1438
+ // the covered high-water mark through `coalescedUntil`. Advancing by the
1439
+ // first sequence duplicates compacted text on the next page. A compact
1440
+ // response length is not an end-of-history signal either: thousands of raw
1441
+ // events may legitimately collapse into one returned event.
1442
+ cursor = maxResumeSequence(page);
1163
1443
  }
1444
+ const densityWindow = trimForwardToGroupTarget(buffer, options.targetGroups);
1445
+ buffer = densityWindow.events;
1164
1446
 
1165
1447
  const oldest = buffer[0] ?? null;
1166
1448
  const newest = buffer[buffer.length - 1] ?? null;
@@ -1168,10 +1450,45 @@ async function loadForwardEventWindow(
1168
1450
  events: buffer,
1169
1451
  oldestSequence: oldest?.sequence ?? null,
1170
1452
  newestSequence: newest ? maxResumeSequence(buffer) : 0,
1171
- hasNewer: buffer.length > 0 && !reachedEnd,
1453
+ hasNewer: buffer.length > 0 && (densityWindow.trimmed || !reachedEnd),
1172
1454
  };
1173
1455
  }
1174
1456
 
1457
+ type DensityWindow = {
1458
+ events: SessionEvent[];
1459
+ trimmed: boolean;
1460
+ };
1461
+
1462
+ /** Keep the newest complete turn groups near an older-history seam. */
1463
+ function trimBackwardToGroupTarget(events: SessionEvent[], targetGroups: number): DensityWindow {
1464
+ if (!Number.isFinite(targetGroups) || targetGroups <= 0 || events.length === 0) {
1465
+ return { events, trimmed: false };
1466
+ }
1467
+ for (let index = events.length - 1; index > 0; index -= 1) {
1468
+ if (!isTurnBoundary(events[index]!)) continue;
1469
+ const candidate = events.slice(index);
1470
+ if (groupCount(candidate) >= targetGroups) {
1471
+ return { events: candidate, trimmed: true };
1472
+ }
1473
+ }
1474
+ return { events, trimmed: false };
1475
+ }
1476
+
1477
+ /** Keep the oldest complete turn groups near a forward-history seam. */
1478
+ function trimForwardToGroupTarget(events: SessionEvent[], targetGroups: number): DensityWindow {
1479
+ if (!Number.isFinite(targetGroups) || targetGroups <= 0 || events.length === 0) {
1480
+ return { events, trimmed: false };
1481
+ }
1482
+ for (let index = 1; index < events.length; index += 1) {
1483
+ if (!isTurnBoundary(events[index]!)) continue;
1484
+ const candidate = events.slice(0, index);
1485
+ if (groupCount(candidate) >= targetGroups) {
1486
+ return { events: candidate, trimmed: true };
1487
+ }
1488
+ }
1489
+ return { events, trimmed: false };
1490
+ }
1491
+
1175
1492
  type PreviousPage = SessionEvent[] & { requested: number };
1176
1493
 
1177
1494
  async function loadPreviousPage(
@@ -1248,18 +1565,22 @@ function isLogStart(event: SessionEvent): boolean {
1248
1565
  return event.type === "session.created" || event.sequence <= 1;
1249
1566
  }
1250
1567
 
1251
- function maxResumeSequence(events: SessionEvent[]): number {
1568
+ function maxResumeSequence(events: readonly SessionEvent[]): number {
1252
1569
  return events.reduce((max, event) => Math.max(max, eventResumeSequence(event)), 0);
1253
1570
  }
1254
1571
 
1255
- function eventResumeSequence(event: SessionEvent): number {
1256
- const payload = asRecord(event.payload);
1257
- const coalescedUntil = Number(payload.coalescedUntil);
1258
- return Math.max(event.sequence, Number.isFinite(coalescedUntil) ? Math.floor(coalescedUntil) : 0);
1572
+ function maxResumeSequenceOrNull(events: readonly SessionEvent[]): number | null {
1573
+ return events.length > 0 ? maxResumeSequence(events) : null;
1259
1574
  }
1260
1575
 
1261
- function asRecord(value: unknown): Record<string, unknown> {
1262
- return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
1576
+ function eventResumeSequence(event: SessionEvent): number {
1577
+ const streamedCoverage = sessionEventStreamCoveredThrough(event);
1578
+ if (streamedCoverage !== null) return streamedCoverage;
1579
+ return typeof event.coveredThrough === "number" &&
1580
+ Number.isSafeInteger(event.coveredThrough) &&
1581
+ event.coveredThrough >= event.sequence
1582
+ ? event.coveredThrough
1583
+ : event.sequence;
1263
1584
  }
1264
1585
 
1265
1586
  function assertAscending(events: SessionEvent[]): void {