@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
@@ -2,6 +2,7 @@ import type {
2
2
  AttachedBrowserBridge,
3
3
  AttachedBrowserDevice,
4
4
  BrowserAction,
5
+ BrowserActionBatch,
5
6
  BrowserActionReceipt,
6
7
  BrowserDiagnosticBatch,
7
8
  BrowserDownload,
@@ -22,6 +23,8 @@ import { interactionControlFailureFromError } from "@opengeni/sdk/interaction";
22
23
  import {
23
24
  BugIcon,
24
25
  ArchiveIcon,
26
+ ArrowLeftIcon,
27
+ ArrowRightIcon,
25
28
  CheckIcon,
26
29
  ChevronDownIcon,
27
30
  CircleAlertIcon,
@@ -50,6 +53,7 @@ import {
50
53
  useCallback,
51
54
  useEffect,
52
55
  useId,
56
+ useLayoutEffect,
53
57
  useMemo,
54
58
  useRef,
55
59
  useState,
@@ -61,17 +65,38 @@ import {
61
65
  import { useAttachedBrowsers } from "../hooks/use-attached-browsers";
62
66
  import { useBrowserIdentities } from "../hooks/use-browser-identities";
63
67
  import { useBrowserDownloads } from "../hooks/use-browser-downloads";
64
- import { useBrowserSession } from "../hooks/use-browser-session";
68
+ import { useBrowserSession, type BrowserFrameInputFence } from "../hooks/use-browser-session";
65
69
  import { useBrowserSessions } from "../hooks/use-browser-sessions";
66
70
  import { useInteractionInterventions } from "../hooks/use-interaction-interventions";
67
71
  import { useSiteAuthConnections } from "../hooks/use-site-auth-connections";
68
72
  import { cn } from "../lib/cn";
69
73
  import { copyTextToClipboard } from "../lib/clipboard";
70
74
  import { formatBytes } from "../lib/format";
71
- import { isSourcePlacementChangedError } from "../lib/interaction-errors";
75
+ import {
76
+ isInteractionControlUnavailable,
77
+ isSourcePlacementChangedError,
78
+ } from "../lib/interaction-errors";
72
79
  import type { EmbeddedBrowserInteractionClientOverride } from "../session-context";
73
80
  import { browserKey, HUMAN_BROWSER_HOME_URL, normalizeBrowserAddress } from "./browser-input";
74
81
  import { InteractionInterventionBanner } from "./interaction-intervention-banner";
82
+ import { BrowserSelectControl } from "./browser-select-control";
83
+ import { useViewerMenuDismiss } from "./use-viewer-menu-dismiss";
84
+
85
+ /** Auto-ending a lost attached-device browser: bounded retries for transient failures only. */
86
+ const STALE_BROWSER_END_MAX_ATTEMPTS = 3;
87
+ const STALE_BROWSER_END_RETRY_BASE_MS = 5_000;
88
+
89
+ /**
90
+ * Delay before retrying an automatic end of a lost attached-device browser, or null to stop.
91
+ * A refusal (4xx, e.g. 409 when the server will not end it) is never retried: re-sending it on
92
+ * every registry refresh looped about one request per second on prod.
93
+ */
94
+ export function staleBrowserEndRetryDelayMs(cause: unknown, failedAttempts: number): number | null {
95
+ const status = (cause as { status?: unknown } | null)?.status;
96
+ if (typeof status === "number" && status >= 400 && status < 500) return null;
97
+ if (failedAttempts >= STALE_BROWSER_END_MAX_ATTEMPTS) return null;
98
+ return STALE_BROWSER_END_RETRY_BASE_MS * 2 ** (failedAttempts - 1);
99
+ }
75
100
 
76
101
  export type BrowserViewerNotification = {
77
102
  kind: "error" | "info";
@@ -79,9 +104,12 @@ export type BrowserViewerNotification = {
79
104
  };
80
105
 
81
106
  export type BrowserViewerProps = EmbeddedBrowserInteractionClientOverride & {
82
- /** The selected OpenGeni agent/session. Peer BrowserSessions stay discoverable. */
107
+ /** The selected Opengeni agent/session. Peer BrowserSessions stay discoverable. */
83
108
  sessionId: string;
84
109
  enabled?: boolean | undefined;
110
+ /** Whether the Browser tab is selected. Keeps the mounted media stream warm
111
+ * while pausing semantic reads behind other dock tabs. */
112
+ active?: boolean | undefined;
85
113
  className?: string | undefined;
86
114
  onNotify?: ((notification: BrowserViewerNotification) => void) | undefined;
87
115
  /** Tests/demos only. Production uses the browser's native WebSocket. */
@@ -111,11 +139,15 @@ type BrowserLaunchChoice =
111
139
  | { kind: "clean" }
112
140
  | { kind: "profile"; identityId: string; baseRevisionId?: string }
113
141
  | { kind: "attached"; device: AttachedBrowserDevice };
142
+ // ArrayBuffer is opaque to React development prop diagnostics; a Uint8Array
143
+ // exposes every image byte as an enumerable property retained in timing entries.
144
+ type BrowserViewportFrame = Omit<BrowserFrame, "data"> & { data: ArrayBuffer };
145
+
114
146
  type PointerStart = {
115
147
  x: number;
116
148
  y: number;
117
149
  pointerId: number;
118
- frame: BrowserFrame;
150
+ frame: BrowserViewportFrame;
119
151
  };
120
152
  type BrowserResumeAttempt = {
121
153
  operationId: string;
@@ -139,6 +171,7 @@ type BrowserDiagnosticsView = {
139
171
  export function BrowserViewer({
140
172
  sessionId,
141
173
  enabled = true,
174
+ active = true,
142
175
  className,
143
176
  onNotify,
144
177
  webSocketFactory,
@@ -173,6 +206,7 @@ export function BrowserViewer({
173
206
  [registry.sessions],
174
207
  );
175
208
  const endedGenerationLossRef = useRef(new Set<string>());
209
+ const endStaleBrowserAttemptsRef = useRef(new Map<string, number>());
176
210
  const endStaleBrowser = registry.end;
177
211
  useEffect(() => {
178
212
  if (!enabled) return;
@@ -186,21 +220,24 @@ export function BrowserViewer({
186
220
  }
187
221
  if (endedGenerationLossRef.current.has(session.id)) continue;
188
222
  endedGenerationLossRef.current.add(session.id);
189
- void endStaleBrowser(session.id).catch(() => {
190
- endedGenerationLossRef.current.delete(session.id);
223
+ void endStaleBrowser(session.id).catch((cause: unknown) => {
224
+ const attempts = (endStaleBrowserAttemptsRef.current.get(session.id) ?? 0) + 1;
225
+ endStaleBrowserAttemptsRef.current.set(session.id, attempts);
226
+ const delay = staleBrowserEndRetryDelayMs(cause, attempts);
227
+ if (delay === null) return;
228
+ setTimeout(() => endedGenerationLossRef.current.delete(session.id), delay);
191
229
  });
192
230
  }
193
231
  }, [enabled, endStaleBrowser, registry.sessions]);
194
- const replacementChromeDevice = useMemo(() => {
195
- if (!attachedGenerationLoss) return null;
196
- return (
197
- attached.devices.find((candidate) => candidate.id === attachedGenerationLoss.deviceId) ?? null
198
- );
199
- }, [attached.devices, attachedGenerationLoss]);
200
232
  const relevant = useMemo(
201
233
  () => registry.relevantSessions.filter((session) => isLiveBrowser(session)),
202
234
  [registry.relevantSessions],
203
235
  );
236
+ const latestRelevantBrowser = registry.relevantSessions[0];
237
+ const unavailableBrowser =
238
+ latestRelevantBrowser?.lifecycle === "lost" || latestRelevantBrowser?.lifecycle === "failed"
239
+ ? latestRelevantBrowser
240
+ : undefined;
204
241
  const [selection, setSelection] = useState<BrowserSelection>(() =>
205
242
  initialBrowserSessionId ? { sessionId: initialBrowserSessionId, pinned: true } : null,
206
243
  );
@@ -232,6 +269,7 @@ export function BrowserViewer({
232
269
  const previousSessionIdRef = useRef(sessionId);
233
270
  const seenInterventionIdsRef = useRef(new Set<string>());
234
271
  const diagnosticRequestRef = useRef(0);
272
+ const viewportFocusHandoffRef = useRef<string | null>(null);
235
273
  const [diagnosticsView, setDiagnosticsView] = useState<BrowserDiagnosticsView | null>(null);
236
274
 
237
275
  const notifyError = useCallback(
@@ -287,6 +325,7 @@ export function BrowserViewer({
287
325
  );
288
326
  const controllerReady = selectedRegistrySession?.lifecycle === "active";
289
327
  const resumeSession = registry.resume;
328
+ const [hasLiveFrame, setHasLiveFrame] = useState(false);
290
329
 
291
330
  const wakeBrowser = useCallback(
292
331
  async (session: BrowserSession, retry = false): Promise<void> => {
@@ -337,17 +376,14 @@ export function BrowserViewer({
337
376
  setResumeAttempt((visible) =>
338
377
  visible?.sessionId === selectedRegistrySession.id ? null : visible,
339
378
  );
340
- return;
341
- }
342
- if (selectedRegistrySession.lifecycle === "suspended") {
343
- void wakeBrowser(selectedRegistrySession);
344
379
  }
345
- }, [selectedRegistrySession, wakeBrowser]);
380
+ }, [selectedRegistrySession]);
346
381
 
347
382
  const browser = useBrowserSession({
348
383
  ...override,
349
384
  browserSessionId: selection?.sessionId ?? null,
350
385
  enabled: enabled && selection !== null && controllerReady,
386
+ semanticObservationEnabled: active && !hasLiveFrame,
351
387
  });
352
388
  useEffect(() => {
353
389
  if (!isSourcePlacementChangedError(browser.error, "browser_session")) return;
@@ -406,19 +442,47 @@ export function BrowserViewer({
406
442
  stream: { format: "jpeg", quality: 76, maxWidth: 1_920, maxHeight: 1_200 },
407
443
  ...(webSocketFactory ? { webSocketFactory } : {}),
408
444
  });
409
- const displayedFrame = frameMatchesSelectedTarget(
445
+ const receivedFrame = frameMatchesSelectedTarget(
410
446
  frames.frame,
411
447
  browser.session,
412
448
  browser.selectedTarget,
413
449
  )
414
450
  ? frames.frame
415
451
  : null;
416
- const supportsLiveFrames = browser.session?.capabilities.liveFrames === true;
417
- const displayConnectionState = supportsLiveFrames
418
- ? frames.state === "live" && !displayedFrame
419
- ? "connecting"
420
- : frames.state
421
- : "semantic";
452
+ const displayedFrame = useMemo<BrowserViewportFrame | null>(
453
+ () => (receivedFrame ? { ...receivedFrame, data: receivedFrame.data.slice().buffer } : null),
454
+ [receivedFrame],
455
+ );
456
+ const frameIsLive = frames.state === "live" && displayedFrame !== null;
457
+ useEffect(() => {
458
+ setHasLiveFrame(frameIsLive);
459
+ }, [frameIsLive]);
460
+ const supportsLiveFrames =
461
+ (browser.session ?? selectedRegistrySession)?.capabilities.liveFrames === true;
462
+ const controlUnavailable = isInteractionControlUnavailable(browser.error);
463
+ const connectionError = controlUnavailable ? browser.error : (frames.error ?? browser.error);
464
+ // A managed controller can reject a stale attachment while the same browser
465
+ // remains healthy. Only extension-attached Chrome requires a new browser on
466
+ // connection-generation loss; an unrelated lost Chrome must not poison the
467
+ // selected managed browser's recovery UI.
468
+ const selectedPlacement = (browser.session ?? selectedRegistrySession)?.placement;
469
+ const selectedChromeGenerationLoss =
470
+ selectedPlacement?.kind === "attached_device" &&
471
+ isAttachedChromeGenerationLossError(connectionError);
472
+ const replacementChromeDevice =
473
+ selectedChromeGenerationLoss && selectedPlacement.kind === "attached_device"
474
+ ? attached.devices.find((candidate) => candidate.id === selectedPlacement.deviceId)
475
+ : null;
476
+ const displayConnectionState =
477
+ connectionError && (!frameIsLive || controlUnavailable)
478
+ ? "error"
479
+ : !browser.session
480
+ ? "connecting"
481
+ : supportsLiveFrames
482
+ ? frames.state === "live" && !displayedFrame
483
+ ? "connecting"
484
+ : frames.state
485
+ : "semantic";
422
486
  const selectedProfile = useMemo(
423
487
  () =>
424
488
  profiles.identities.find(
@@ -735,25 +799,32 @@ export function BrowserViewer({
735
799
  );
736
800
  }
737
801
  if (liveSessions.length === 0) {
738
- if (renderEmpty) return <>{renderEmpty(() => createBrowser(), creating)}</>;
802
+ if (renderEmpty && !unavailableBrowser)
803
+ return <>{renderEmpty(() => createBrowser(), creating)}</>;
739
804
  return (
740
805
  <div
741
806
  className={cn("flex h-full min-h-0 items-center justify-center bg-og-bg p-6", className)}
742
807
  >
743
808
  <div className="max-w-sm text-center">
744
- <span className="mx-auto grid size-10 place-items-center rounded-og-md border border-og-border bg-og-surface-1 text-og-muted">
809
+ <span className="mx-auto grid size-10 place-items-center rounded-og-md border border-og-border bg-og-surface-1 text-og-fg-muted">
745
810
  <Globe2Icon className="size-4.5" />
746
811
  </span>
747
- <p className="mt-3 text-og-menu font-medium text-og-fg">
748
- {attachedGenerationLoss
749
- ? "Chrome reconnected—open a fresh browser/desktop."
750
- : "No browser open"}
751
- </p>
752
- <p className="mt-1 text-og-control leading-5 text-og-muted">
753
- {attachedGenerationLoss
754
- ? "This Chrome profile is live again, but the previous browser cannot move to the new connection. Use Browser → New browser → Connected Chrome."
755
- : "A browser appears here when this agent—or another agent in the workspace—opens one."}
756
- </p>
812
+ {unavailableBrowser && !attachedGenerationLoss ? (
813
+ <BrowserUnavailableNotice session={unavailableBrowser} />
814
+ ) : (
815
+ <>
816
+ <p className="mt-3 text-og-menu font-medium text-og-fg">
817
+ {attachedGenerationLoss
818
+ ? "Chrome reconnected—open a fresh browser/desktop."
819
+ : "No browser open"}
820
+ </p>
821
+ <p className="mt-1 text-og-control leading-5 text-og-fg-muted">
822
+ {attachedGenerationLoss
823
+ ? "This Chrome profile is live again, but the previous browser cannot move to the new connection. Use Browser → New browser → Connected Chrome."
824
+ : "A browser appears here when this agent—or another agent in the workspace—opens one."}
825
+ </p>
826
+ </>
827
+ )}
757
828
  <BrowserLaunchMenu
758
829
  attachedBridges={onlineAttachedBridges}
759
830
  attachedDevices={attached.devices}
@@ -766,7 +837,7 @@ export function BrowserViewer({
766
837
  prominent
767
838
  />
768
839
  {registry.error ? (
769
- <p className="mt-3 text-og-control text-og-status-error">{registry.error.message}</p>
840
+ <p className="mt-3 text-og-control text-og-danger">{registry.error.message}</p>
770
841
  ) : null}
771
842
  </div>
772
843
  </div>
@@ -821,6 +892,7 @@ export function BrowserViewer({
821
892
  />
822
893
  {!selectedRegistrySession ? (
823
894
  <BrowserUnselectedPanel
895
+ unavailableBrowser={unavailableBrowser}
824
896
  peerCount={liveSessions.length}
825
897
  creating={creating}
826
898
  onCreate={() => createBrowser()}
@@ -864,6 +936,22 @@ export function BrowserViewer({
864
936
  .act({ type: "navigate", url })
865
937
  .catch((cause) => notifyError(cause, "Could not navigate."))
866
938
  }
939
+ onHistory={async (direction) => {
940
+ try {
941
+ const action = { type: "history", direction } as const;
942
+ if (receivedFrame) await browser.actFromFrame(action, receivedFrame);
943
+ else {
944
+ if (!browser.observation)
945
+ throw new Error("The browser page is not ready for input.");
946
+ await browser.actFromObservation(action, browser.observation);
947
+ }
948
+ } catch (cause) {
949
+ notifyError(
950
+ cause,
951
+ direction === "back" ? "Could not go back." : "Could not go forward.",
952
+ );
953
+ }
954
+ }}
867
955
  onReload={() => {
868
956
  const url = browser.selectedTarget?.url;
869
957
  if (url)
@@ -872,52 +960,136 @@ export function BrowserViewer({
872
960
  .catch((cause) => notifyError(cause, "Could not reload."));
873
961
  }}
874
962
  />
875
- <BrowserViewport
876
- frame={displayedFrame}
877
- connectionState={displayConnectionState}
878
- supportsLiveFrames={supportsLiveFrames}
879
- connectionError={frames.error}
880
- observation={browser.observation}
881
- mutating={browser.mutating || savingProfile}
882
- activityLabel={savingProfile ? "Saving browser version…" : undefined}
883
- clipboardEnabled={browser.session?.capabilities.clipboard === true}
884
- onAction={async (action, frame) => {
885
- const receipt = frame
886
- ? await browser.actFromFrame(action, frame)
887
- : await browser.act(action);
888
- if (receipt.state !== "completed") {
889
- throw new Error(receipt.error?.message ?? "Browser input did not complete.");
890
- }
891
- return receipt;
892
- }}
893
- onReadClipboard={browser.readClipboard}
894
- onReconnect={
895
- attachedGenerationLoss || isAttachedChromeGenerationLossError(frames.error)
896
- ? () => {
897
- if (replacementChromeDevice) {
898
- createBrowser({ kind: "attached", device: replacementChromeDevice });
963
+ {browser.inputFailure ? (
964
+ <div
965
+ role="alert"
966
+ aria-label="Browser input status"
967
+ className="flex shrink-0 items-center gap-2 border-t border-og-border bg-og-surface-1 px-3 py-2 text-og-xs"
968
+ >
969
+ <CircleAlertIcon className="size-4 shrink-0 text-og-status-failed" />
970
+ <div className="min-w-0 flex-1">
971
+ <span className="font-medium">
972
+ {browser.inputFailure.state === "failed"
973
+ ? "Browser input failed"
974
+ : "Input result unknown"}
975
+ </span>
976
+ <p className="text-og-fg-subtle">
977
+ {browser.inputFailure.error?.message ?? "Inspect the page before continuing."}
978
+ </p>
979
+ </div>
980
+ <button
981
+ type="button"
982
+ onClick={() => void browser.refresh()}
983
+ className="shrink-0 rounded border border-og-border px-2 py-1 transition hover:bg-og-surface-2"
984
+ >
985
+ Check browser
986
+ </button>
987
+ </div>
988
+ ) : null}
989
+ <div className="relative flex min-h-0 min-w-0 flex-1 overflow-hidden">
990
+ <BrowserViewport
991
+ // A new control scope must discard every buffered gesture and queued action.
992
+ key={JSON.stringify([
993
+ selection?.sessionId,
994
+ browser.selectedTarget?.id,
995
+ browser.selectedTarget?.targetGeneration,
996
+ browser.selectedTarget?.documentGeneration,
997
+ frames.attachment?.controllerGeneration,
998
+ ])}
999
+ focusHandoffRef={viewportFocusHandoffRef}
1000
+ focusScope={JSON.stringify([
1001
+ selection?.sessionId,
1002
+ browser.selectedTarget?.id,
1003
+ browser.selectedTarget?.targetGeneration,
1004
+ ])}
1005
+ frame={displayedFrame}
1006
+ connectionState={displayConnectionState}
1007
+ supportsLiveFrames={supportsLiveFrames}
1008
+ connectionError={connectionError}
1009
+ controlUnavailable={controlUnavailable}
1010
+ observation={browser.observation}
1011
+ mutating={browser.mutating || savingProfile}
1012
+ activityLabel={savingProfile ? "Saving browser version…" : undefined}
1013
+ clipboardEnabled={browser.session?.capabilities.clipboard === true}
1014
+ inputBatchAttachment={frames.attachment}
1015
+ onAction={async (action, frame) => {
1016
+ const focusedInput =
1017
+ frame &&
1018
+ action.type === "pointer" &&
1019
+ action.action === "click" &&
1020
+ (action.button === undefined || action.button === "left") &&
1021
+ frames.attachment?.focusedInputObservations === true &&
1022
+ frames.attachment.browserSessionId === frame.browserSessionId &&
1023
+ frames.attachment.controllerGeneration === frame.controllerGeneration &&
1024
+ frames.attachment.targetId === frame.targetId &&
1025
+ Date.parse(frames.attachment.expiresAt) > Date.now();
1026
+ const receipt = frame
1027
+ ? await browser.actFromFrame(
1028
+ action,
1029
+ frame,
1030
+ undefined,
1031
+ focusedInput ? "input" : "none",
1032
+ )
1033
+ : await browser.act(action);
1034
+ if (receipt.state !== "completed") {
1035
+ throw new Error(receipt.error?.message ?? "Browser input did not complete.");
1036
+ }
1037
+ return receipt;
1038
+ }}
1039
+ onReadClipboard={browser.readClipboard}
1040
+ onObserveForInput={browser.observeForInput}
1041
+ onSelectFromObservation={browser.actFromObservation}
1042
+ onReconnect={
1043
+ selectedChromeGenerationLoss
1044
+ ? () => {
1045
+ if (replacementChromeDevice) {
1046
+ createBrowser({ kind: "attached", device: replacementChromeDevice });
1047
+ }
899
1048
  }
900
- }
901
- : frames.reconnect
902
- }
903
- reconnectLabel={
904
- (attachedGenerationLoss || isAttachedChromeGenerationLossError(frames.error)) &&
905
- replacementChromeDevice
906
- ? "Open a fresh Connected Chrome"
907
- : undefined
908
- }
909
- reconnectMessage={
910
- attachedGenerationLoss || isAttachedChromeGenerationLossError(frames.error)
911
- ? "Chrome reconnected—open a fresh browser/desktop."
912
- : undefined
913
- }
914
- onError={(cause) => notifyError(cause, "Browser input failed.")}
915
- />
1049
+ : () => {
1050
+ void browser.refresh();
1051
+ frames.reconnect();
1052
+ }
1053
+ }
1054
+ reconnectLabel={
1055
+ selectedChromeGenerationLoss && replacementChromeDevice
1056
+ ? "Open a fresh Connected Chrome"
1057
+ : undefined
1058
+ }
1059
+ reconnectMessage={
1060
+ selectedChromeGenerationLoss
1061
+ ? "Chrome reconnected—open a fresh browser/desktop."
1062
+ : undefined
1063
+ }
1064
+ onError={(cause) => notifyError(cause, "Browser input failed.")}
1065
+ />
1066
+ {diagnosticsView ? (
1067
+ <BrowserDiagnosticsDrawer
1068
+ session={browser.session}
1069
+ profile={selectedProfile}
1070
+ target={browser.selectedTarget}
1071
+ observation={browser.observation}
1072
+ baseRevisionOrdinal={baseRevisionOrdinal}
1073
+ state={diagnosticsView}
1074
+ downloads={downloads.downloads}
1075
+ downloadsEnabled={browser.session?.capabilities.downloads === true}
1076
+ downloadsLoading={downloads.loading}
1077
+ downloadsRefreshing={downloads.refreshing}
1078
+ downloadsError={downloads.error}
1079
+ savingDownloadIds={downloads.savingDownloadIds}
1080
+ onSaveDownload={saveDownload}
1081
+ onRefreshDownloads={() => void downloads.refresh()}
1082
+ onRefresh={refreshDiagnostics}
1083
+ onClose={closeDiagnostics}
1084
+ />
1085
+ ) : null}
1086
+ </div>
916
1087
  <BrowserStatusBar
917
1088
  session={browser.session}
918
1089
  profile={selectedProfile}
919
1090
  target={browser.selectedTarget}
920
1091
  observation={browser.observation}
1092
+ inputFailure={browser.inputFailure}
921
1093
  connectionState={displayConnectionState}
922
1094
  refreshing={registry.refreshing}
923
1095
  diagnosticsOpen={diagnosticsView !== null}
@@ -933,26 +1105,6 @@ export function BrowserViewer({
933
1105
  />
934
1106
  </>
935
1107
  )}
936
- {diagnosticsView ? (
937
- <BrowserDiagnosticsDrawer
938
- session={browser.session}
939
- profile={selectedProfile}
940
- target={browser.selectedTarget}
941
- observation={browser.observation}
942
- baseRevisionOrdinal={baseRevisionOrdinal}
943
- state={diagnosticsView}
944
- downloads={downloads.downloads}
945
- downloadsEnabled={browser.session?.capabilities.downloads === true}
946
- downloadsLoading={downloads.loading}
947
- downloadsRefreshing={downloads.refreshing}
948
- downloadsError={downloads.error}
949
- savingDownloadIds={downloads.savingDownloadIds}
950
- onSaveDownload={saveDownload}
951
- onRefreshDownloads={() => void downloads.refresh()}
952
- onRefresh={refreshDiagnostics}
953
- onClose={closeDiagnostics}
954
- />
955
- ) : null}
956
1108
  </div>
957
1109
  );
958
1110
  }
@@ -980,20 +1132,20 @@ function BrowserLifecyclePanel(props: {
980
1132
  return (
981
1133
  <div className="grid min-h-0 flex-1 place-items-center bg-og-bg p-6 text-center">
982
1134
  <div className="max-w-sm">
983
- <span className="mx-auto grid size-10 place-items-center rounded-og-md border border-og-border bg-og-surface-1 text-og-muted">
1135
+ <span className="mx-auto grid size-10 place-items-center rounded-og-md border border-og-border bg-og-surface-1 text-og-fg-muted">
984
1136
  {failed ? (
985
- <CircleAlertIcon className="size-4.5 text-og-status-error" />
1137
+ <CircleAlertIcon className="size-4.5 text-og-danger" />
986
1138
  ) : (
987
1139
  <LoaderCircleIcon className="size-4.5 animate-spin" />
988
1140
  )}
989
1141
  </span>
990
1142
  <p className="mt-3 text-og-menu font-medium text-og-fg">{label}</p>
991
- <p className="mt-1 text-og-control leading-5 text-og-muted">{detail}</p>
1143
+ <p className="mt-1 text-og-control leading-5 text-og-fg-muted">{detail}</p>
992
1144
  {failed || (props.session.lifecycle === "suspended" && !props.attempt?.running) ? (
993
1145
  <button
994
1146
  type="button"
995
1147
  onClick={props.onResume}
996
- className="mt-4 inline-flex h-8 items-center rounded-og-sm border border-og-border bg-og-surface-1 px-3 text-og-control font-medium text-og-fg transition hover:bg-og-surface-2"
1148
+ className="mt-4 inline-flex h-8 items-center rounded-og-sm border border-og-primary-border bg-og-primary px-3 text-og-control font-medium text-og-primary-fg transition hover:bg-og-primary-hover"
997
1149
  >
998
1150
  Open browser
999
1151
  </button>
@@ -1030,7 +1182,7 @@ function BrowserToolbar(props: {
1030
1182
  onRefresh: () => void;
1031
1183
  setupUrl?: string | undefined;
1032
1184
  }) {
1033
- const detailsRef = useRef<HTMLDetailsElement | null>(null);
1185
+ const detailsRef = useViewerMenuDismiss();
1034
1186
  const selected = props.sessions.find((session) => session.id === props.selectedSessionId);
1035
1187
  const current = props.sessions.filter((session) => props.relevantSessionIds.has(session.id));
1036
1188
  const others = props.sessions.filter((session) => !props.relevantSessionIds.has(session.id));
@@ -1039,34 +1191,36 @@ function BrowserToolbar(props: {
1039
1191
  detailsRef.current?.removeAttribute("open");
1040
1192
  };
1041
1193
  return (
1042
- <div className="flex h-10 shrink-0 items-center gap-1.5 border-b border-og-border bg-og-surface-0 px-2">
1043
- <details ref={detailsRef} className="relative min-w-0">
1194
+ <div className="flex h-10 shrink-0 items-center gap-1.5 border-b border-og-border bg-og-surface-1 px-2">
1195
+ <details ref={detailsRef} className="min-w-0">
1044
1196
  <summary className="flex h-7 max-w-52 cursor-pointer list-none items-center gap-2 rounded-og-sm px-2 text-og-control text-og-fg transition hover:bg-og-surface-2 [&::-webkit-details-marker]:hidden">
1045
- <Globe2Icon className="size-3.5 shrink-0 text-og-muted" />
1197
+ <Globe2Icon className="size-3.5 shrink-0 text-og-fg-muted" />
1046
1198
  <span className="truncate font-medium">{selected?.name ?? "Browser"}</span>
1047
1199
  {(props.interventionCounts.get(selected?.id ?? "") ?? 0) > 0 ? (
1048
1200
  <span className="size-1.5 shrink-0 rounded-full bg-og-status-waiting" />
1049
1201
  ) : null}
1050
- <ChevronDownIcon className="size-3 shrink-0 text-og-subtle" />
1202
+ <ChevronDownIcon className="size-3 shrink-0 text-og-fg-subtle" />
1051
1203
  </summary>
1052
- <div className="absolute left-0 top-8 z-30 w-72 overflow-hidden rounded-og-md border border-og-border bg-og-surface-1 p-1 shadow-xl">
1053
- <BrowserSessionGroup
1054
- label="Current agent"
1055
- sessions={current}
1056
- identities={props.identities}
1057
- selectedId={props.selectedSessionId}
1058
- interventionCounts={props.interventionCounts}
1059
- onSelect={choose}
1060
- />
1061
- <BrowserSessionGroup
1062
- label="Workspace browsers"
1063
- sessions={others}
1064
- identities={props.identities}
1065
- selectedId={props.selectedSessionId}
1066
- interventionCounts={props.interventionCounts}
1067
- onSelect={choose}
1068
- />
1069
- <div className="mt-1 flex gap-1 border-t border-og-border pt-1">
1204
+ <div className="absolute left-2 top-10 z-30 flex max-h-[calc(100%-3rem)] w-72 max-w-[calc(100%-1rem)] flex-col overflow-hidden rounded-og-md border border-og-border bg-og-surface-1 p-1 shadow-xl">
1205
+ <div className="min-h-0 max-h-96 overflow-y-auto overscroll-contain">
1206
+ <BrowserSessionGroup
1207
+ label="Current agent"
1208
+ sessions={current}
1209
+ identities={props.identities}
1210
+ selectedId={props.selectedSessionId}
1211
+ interventionCounts={props.interventionCounts}
1212
+ onSelect={choose}
1213
+ />
1214
+ <BrowserSessionGroup
1215
+ label="Workspace browsers"
1216
+ sessions={others}
1217
+ identities={props.identities}
1218
+ selectedId={props.selectedSessionId}
1219
+ interventionCounts={props.interventionCounts}
1220
+ onSelect={choose}
1221
+ />
1222
+ </div>
1223
+ <div className="mt-1 flex shrink-0 gap-1 border-t border-og-border pt-1">
1070
1224
  {current.length > 0 ? (
1071
1225
  <MenuButton onClick={props.onFollow}>Follow agent</MenuButton>
1072
1226
  ) : null}
@@ -1076,7 +1230,7 @@ function BrowserToolbar(props: {
1076
1230
  </div>
1077
1231
  </div>
1078
1232
  </details>
1079
- <span className="min-w-0 flex-1 truncate text-og-xs text-og-subtle">
1233
+ <span className="min-w-0 flex-1 truncate text-og-xs text-og-fg-subtle">
1080
1234
  {placementLabel(selected)}
1081
1235
  </span>
1082
1236
  <BrowserProfileMenu
@@ -1093,7 +1247,7 @@ function BrowserToolbar(props: {
1093
1247
  <button
1094
1248
  type="button"
1095
1249
  onClick={props.onRefresh}
1096
- className="grid size-7 place-items-center rounded-og-sm text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg"
1250
+ className="grid size-7 shrink-0 place-items-center rounded-og-sm text-og-fg-muted transition hover:bg-og-surface-2 hover:text-og-fg"
1097
1251
  aria-label="Refresh browsers"
1098
1252
  >
1099
1253
  <RefreshCwIcon className={cn("size-3.5", props.refreshing && "animate-spin")} />
@@ -1113,6 +1267,7 @@ function BrowserToolbar(props: {
1113
1267
  }
1114
1268
 
1115
1269
  function BrowserUnselectedPanel(props: {
1270
+ unavailableBrowser: BrowserSession | undefined;
1116
1271
  peerCount: number;
1117
1272
  creating: boolean;
1118
1273
  onCreate: () => void;
@@ -1120,11 +1275,15 @@ function BrowserUnselectedPanel(props: {
1120
1275
  return (
1121
1276
  <div className="grid min-h-0 flex-1 place-items-center bg-og-bg p-6 text-center">
1122
1277
  <div className="max-w-sm">
1123
- <span className="mx-auto grid size-10 place-items-center rounded-og-md border border-og-border bg-og-surface-1 text-og-muted">
1278
+ <span className="mx-auto grid size-10 place-items-center rounded-og-md border border-og-border bg-og-surface-1 text-og-fg-muted">
1124
1279
  <Globe2Icon className="size-4.5" />
1125
1280
  </span>
1126
- <p className="mt-3 text-og-menu font-medium text-og-fg">No browser for this agent</p>
1127
- <p className="mt-1 text-og-control leading-5 text-og-muted">
1281
+ {props.unavailableBrowser ? (
1282
+ <BrowserUnavailableNotice session={props.unavailableBrowser} />
1283
+ ) : (
1284
+ <p className="mt-3 text-og-menu font-medium text-og-fg">No browser for this agent</p>
1285
+ )}
1286
+ <p className="mt-1 text-og-control leading-5 text-og-fg-muted">
1128
1287
  {props.peerCount === 1
1129
1288
  ? "One workspace browser is available from the browser menu."
1130
1289
  : `${props.peerCount} workspace browsers are available from the browser menu.`}
@@ -1133,7 +1292,7 @@ function BrowserUnselectedPanel(props: {
1133
1292
  type="button"
1134
1293
  disabled={props.creating}
1135
1294
  onClick={props.onCreate}
1136
- className="mt-4 inline-flex h-8 items-center gap-1.5 rounded-og-sm border border-og-border bg-og-surface-1 px-3 text-og-control font-medium text-og-fg transition hover:bg-og-surface-2 disabled:cursor-not-allowed disabled:opacity-50"
1295
+ className="mt-4 inline-flex h-8 items-center gap-1.5 rounded-og-sm border border-og-primary-border bg-og-primary px-3 text-og-control font-medium text-og-primary-fg transition hover:bg-og-primary-hover disabled:cursor-not-allowed disabled:opacity-50"
1137
1296
  >
1138
1297
  <PlusIcon className="size-3.5" />
1139
1298
  {props.creating ? "Opening…" : "New browser"}
@@ -1143,6 +1302,27 @@ function BrowserUnselectedPanel(props: {
1143
1302
  );
1144
1303
  }
1145
1304
 
1305
+ function BrowserUnavailableNotice({ session }: { session: BrowserSession }) {
1306
+ const reason =
1307
+ session.failureCode === "provider_deadline_rotation"
1308
+ ? "The computer running it reached its time limit."
1309
+ : session.failureCode === "source_placement_changed"
1310
+ ? "This chat moved to another computer."
1311
+ : session.failureCode === "idle_released"
1312
+ ? "It stopped after it went unused."
1313
+ : session.lifecycle === "failed"
1314
+ ? "It stopped unexpectedly."
1315
+ : "The connection to it was lost.";
1316
+ return (
1317
+ <div role="status">
1318
+ <p className="mt-3 text-og-menu font-medium text-og-fg">Browser unavailable</p>
1319
+ <p className="mt-1 break-words text-og-control leading-5 text-og-fg-muted">
1320
+ {session.name}. {reason} Open a new browser to continue.
1321
+ </p>
1322
+ </div>
1323
+ );
1324
+ }
1325
+
1146
1326
  function BrowserSessionGroup(props: {
1147
1327
  label: string;
1148
1328
  sessions: BrowserSession[];
@@ -1154,7 +1334,7 @@ function BrowserSessionGroup(props: {
1154
1334
  if (props.sessions.length === 0) return null;
1155
1335
  return (
1156
1336
  <div className="py-1">
1157
- <p className="px-2 pb-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-subtle">
1337
+ <p className="px-2 pb-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-fg-subtle">
1158
1338
  {props.label}
1159
1339
  </p>
1160
1340
  {props.sessions.map((session) => {
@@ -1164,6 +1344,8 @@ function BrowserSessionGroup(props: {
1164
1344
  <button
1165
1345
  key={session.id}
1166
1346
  type="button"
1347
+ aria-label={`${session.name}${identity ? ` · ${identity.name}` : ""} · ${placementLabel(session)}`}
1348
+ aria-pressed={session.id === props.selectedId}
1167
1349
  onClick={() => props.onSelect(session.id)}
1168
1350
  className={cn(
1169
1351
  "flex w-full items-center gap-2 rounded-og-sm px-2 py-1.5 text-left transition hover:bg-og-surface-2",
@@ -1178,7 +1360,7 @@ function BrowserSessionGroup(props: {
1178
1360
  />
1179
1361
  <span className="min-w-0 flex-1">
1180
1362
  <span className="block truncate text-og-control text-og-fg">{session.name}</span>
1181
- <span className="block truncate text-og-xs text-og-subtle">
1363
+ <span className="block truncate text-og-xs text-og-fg-subtle">
1182
1364
  {identity ? `${identity.name} · ` : ""}
1183
1365
  {placementLabel(session)}
1184
1366
  </span>
@@ -1206,7 +1388,7 @@ function BrowserLaunchMenu(props: {
1206
1388
  setupUrl?: string | undefined;
1207
1389
  prominent?: boolean;
1208
1390
  }) {
1209
- const detailsRef = useRef<HTMLDetailsElement | null>(null);
1391
+ const detailsRef = useViewerMenuDismiss();
1210
1392
  const choose = (choice?: BrowserLaunchChoice) => {
1211
1393
  detailsRef.current?.removeAttribute("open");
1212
1394
  props.onCreate(choice);
@@ -1218,13 +1400,13 @@ function BrowserLaunchMenu(props: {
1218
1400
  const activeIdentities = props.identities.filter((identity) => identity.status === "active");
1219
1401
  const archivedIdentities = props.identities.filter((identity) => identity.status === "archived");
1220
1402
  return (
1221
- <details ref={detailsRef} className={cn("relative", props.prominent && "mt-4 inline-block")}>
1403
+ <details ref={detailsRef} className={cn(props.prominent && "mt-4 inline-block")}>
1222
1404
  <summary
1223
1405
  onClick={(event) => {
1224
1406
  if (busy) event.preventDefault();
1225
1407
  }}
1226
1408
  className={cn(
1227
- "cursor-pointer list-none items-center justify-center gap-1.5 text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg [&::-webkit-details-marker]:hidden",
1409
+ "cursor-pointer list-none items-center justify-center gap-1.5 text-og-fg-muted transition hover:bg-og-surface-2 hover:text-og-fg [&::-webkit-details-marker]:hidden",
1228
1410
  props.prominent
1229
1411
  ? "inline-flex h-8 rounded-og-sm border border-og-border bg-og-surface-1 px-3 text-og-control font-medium text-og-fg"
1230
1412
  : "grid size-7 place-items-center rounded-og-sm",
@@ -1242,13 +1424,13 @@ function BrowserLaunchMenu(props: {
1242
1424
  </summary>
1243
1425
  <div
1244
1426
  className={cn(
1245
- "absolute z-40 w-72 overflow-hidden rounded-og-md border border-og-border bg-og-surface-1 p-1 text-left shadow-xl",
1246
- props.prominent ? "left-1/2 top-10 -translate-x-1/2" : "right-0 top-8",
1427
+ "absolute z-40 max-h-[calc(100%-3rem)] w-72 max-w-[calc(100%-1rem)] overflow-y-auto rounded-og-md border border-og-border bg-og-surface-1 p-1 text-left shadow-xl",
1428
+ props.prominent ? "left-1/2 top-2 -translate-x-1/2" : "right-2 top-10",
1247
1429
  )}
1248
1430
  >
1249
1431
  {props.attachedDevices.length > 0 || props.attachedBridges.length > 0 ? (
1250
1432
  <div className="pb-1">
1251
- <p className="px-2 pb-1 pt-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-subtle">
1433
+ <p className="px-2 pb-1 pt-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-fg-subtle">
1252
1434
  Connected Chrome
1253
1435
  </p>
1254
1436
  {props.attachedDevices.map((device) => (
@@ -1258,12 +1440,12 @@ function BrowserLaunchMenu(props: {
1258
1440
  onClick={() => choose({ kind: "attached", device })}
1259
1441
  className="flex w-full items-center gap-2 rounded-og-sm px-2 py-2 text-left transition hover:bg-og-surface-2"
1260
1442
  >
1261
- <MonitorIcon className="size-3.5 shrink-0 text-og-muted" />
1443
+ <MonitorIcon className="size-3.5 shrink-0 text-og-fg-muted" />
1262
1444
  <span className="min-w-0 flex-1">
1263
1445
  <span className="block truncate text-og-control text-og-fg">
1264
1446
  {device.profileLabel ?? device.name}
1265
1447
  </span>
1266
- <span className="block truncate text-og-xs text-og-subtle">
1448
+ <span className="block truncate text-og-xs text-og-fg-subtle">
1267
1449
  {device.browserName} · {device.tabCount} open{" "}
1268
1450
  {device.tabCount === 1 ? "tab" : "tabs"}
1269
1451
  </span>
@@ -1280,27 +1462,27 @@ function BrowserLaunchMenu(props: {
1280
1462
  onClick={() => detailsRef.current?.removeAttribute("open")}
1281
1463
  className="flex w-full items-center gap-2 rounded-og-sm px-2 py-2 text-left transition hover:bg-og-surface-2"
1282
1464
  >
1283
- <PlusIcon className="size-3.5 shrink-0 text-og-muted" />
1465
+ <PlusIcon className="size-3.5 shrink-0 text-og-fg-muted" />
1284
1466
  <span className="min-w-0 flex-1">
1285
1467
  <span className="block truncate text-og-control text-og-fg">
1286
1468
  {props.attachedDevices.length > 0
1287
1469
  ? "Connect another Chrome profile"
1288
1470
  : "Connect this Chrome profile"}
1289
1471
  </span>
1290
- <span className="block truncate text-og-xs text-og-subtle">
1472
+ <span className="block truncate text-og-xs text-og-fg-subtle">
1291
1473
  Machine connected · Chrome extension missing
1292
1474
  </span>
1293
1475
  </span>
1294
1476
  </a>
1295
1477
  ) : (
1296
1478
  <div className="flex w-full items-center gap-2 px-2 py-2 text-left">
1297
- <MonitorIcon className="size-3.5 shrink-0 text-og-muted" />
1479
+ <MonitorIcon className="size-3.5 shrink-0 text-og-fg-muted" />
1298
1480
  <span className="min-w-0 flex-1">
1299
1481
  <span className="block truncate text-og-control text-og-fg">
1300
1482
  Chrome connection ready
1301
1483
  </span>
1302
- <span className="block text-og-xs leading-4 text-og-subtle">
1303
- Install the OpenGeni Browser extension in the profile you want to use.
1484
+ <span className="block text-og-xs leading-4 text-og-fg-subtle">
1485
+ Install the Opengeni Browser extension in the profile you want to use.
1304
1486
  </span>
1305
1487
  </span>
1306
1488
  </div>
@@ -1310,7 +1492,7 @@ function BrowserLaunchMenu(props: {
1310
1492
  ) : null}
1311
1493
  <p
1312
1494
  className={cn(
1313
- "px-2 pb-1 pt-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-subtle",
1495
+ "px-2 pb-1 pt-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-fg-subtle",
1314
1496
  (props.attachedDevices.length > 0 || props.attachedBridges.length > 0) &&
1315
1497
  "border-t border-og-border",
1316
1498
  )}
@@ -1322,10 +1504,12 @@ function BrowserLaunchMenu(props: {
1322
1504
  onClick={() => choose({ kind: "clean" })}
1323
1505
  className="flex w-full items-center gap-2 rounded-og-sm px-2 py-2 text-left transition hover:bg-og-surface-2"
1324
1506
  >
1325
- <Globe2Icon className="size-3.5 text-og-muted" />
1507
+ <Globe2Icon className="size-3.5 text-og-fg-muted" />
1326
1508
  <span>
1327
1509
  <span className="block text-og-control text-og-fg">Fresh browser</span>
1328
- <span className="block text-og-xs text-og-subtle">Visual browser · no saved state</span>
1510
+ <span className="block text-og-xs text-og-fg-subtle">
1511
+ Visual browser · no saved state
1512
+ </span>
1329
1513
  </span>
1330
1514
  </button>
1331
1515
  {activeIdentities.length > 0 ? (
@@ -1337,10 +1521,10 @@ function BrowserLaunchMenu(props: {
1337
1521
  onClick={() => choose({ kind: "profile", identityId: identity.id })}
1338
1522
  className="flex w-full items-center gap-2 rounded-og-sm px-2 py-2 text-left transition hover:bg-og-surface-2"
1339
1523
  >
1340
- <UserRoundIcon className="size-3.5 text-og-muted" />
1524
+ <UserRoundIcon className="size-3.5 text-og-fg-muted" />
1341
1525
  <span className="min-w-0">
1342
1526
  <span className="block truncate text-og-control text-og-fg">{identity.name}</span>
1343
- <span className="block text-og-xs text-og-subtle">
1527
+ <span className="block text-og-xs text-og-fg-subtle">
1344
1528
  {identity.defaultRevisionId
1345
1529
  ? `${identity.revisionCount} saved version${identity.revisionCount === 1 ? "" : "s"}`
1346
1530
  : "Ready for first sign-in"}
@@ -1352,7 +1536,7 @@ function BrowserLaunchMenu(props: {
1352
1536
  ) : null}
1353
1537
  {archivedIdentities.length > 0 ? (
1354
1538
  <details className="mt-1 border-t border-og-border pt-1">
1355
- <summary className="flex cursor-pointer list-none items-center gap-1.5 rounded-og-sm px-2 py-1.5 text-og-xs text-og-subtle transition hover:bg-og-surface-2 hover:text-og-fg [&::-webkit-details-marker]:hidden">
1539
+ <summary className="flex cursor-pointer list-none items-center gap-1.5 rounded-og-sm px-2 py-1.5 text-og-xs text-og-fg-subtle transition hover:bg-og-surface-2 hover:text-og-fg [&::-webkit-details-marker]:hidden">
1356
1540
  <ArchiveIcon className="size-3" /> Hidden profiles · {archivedIdentities.length}
1357
1541
  <ChevronDownIcon className="ml-auto size-3" />
1358
1542
  </summary>
@@ -1364,7 +1548,7 @@ function BrowserLaunchMenu(props: {
1364
1548
  onClick={() => void restore(identity)}
1365
1549
  className="flex w-full items-center gap-2 rounded-og-sm px-2 py-2 text-left transition hover:bg-og-surface-2 disabled:opacity-50"
1366
1550
  >
1367
- <RotateCcwIcon className="size-3.5 text-og-muted" />
1551
+ <RotateCcwIcon className="size-3.5 text-og-fg-muted" />
1368
1552
  <span className="min-w-0 flex-1 truncate text-og-control text-og-fg">
1369
1553
  Restore {identity.name}
1370
1554
  </span>
@@ -1391,7 +1575,7 @@ function BrowserProfileMenu(props: {
1391
1575
  ) => Promise<boolean>;
1392
1576
  onOpenVersion: (identityId: string, baseRevisionId: string) => void;
1393
1577
  }) {
1394
- const detailsRef = useRef<HTMLDetailsElement | null>(null);
1578
+ const detailsRef = useViewerMenuDismiss();
1395
1579
  const nameInputId = useId();
1396
1580
  const [name, setName] = useState("");
1397
1581
  const attached = props.session?.placement.kind === "attached_device";
@@ -1422,8 +1606,8 @@ function BrowserProfileMenu(props: {
1422
1606
  : "unsaved"
1423
1607
  : "temporary";
1424
1608
  return (
1425
- <details ref={detailsRef} className="relative min-w-0">
1426
- <summary className="flex h-7 max-w-40 cursor-pointer list-none items-center gap-1.5 rounded-og-sm px-2 text-og-xs text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg [&::-webkit-details-marker]:hidden">
1609
+ <details ref={detailsRef} className="min-w-0">
1610
+ <summary className="flex h-7 max-w-40 cursor-pointer list-none items-center gap-1.5 rounded-og-sm px-2 text-og-xs text-og-fg-muted transition hover:bg-og-surface-2 hover:text-og-fg [&::-webkit-details-marker]:hidden">
1427
1611
  {props.saving ? (
1428
1612
  <LoaderCircleIcon className="size-3 animate-spin" />
1429
1613
  ) : attached ? (
@@ -1435,13 +1619,13 @@ function BrowserProfileMenu(props: {
1435
1619
  {attached ? props.session?.name : (props.identity?.name ?? "Temporary")}
1436
1620
  </span>
1437
1621
  {attached || props.identity ? (
1438
- <span className="shrink-0 text-og-subtle">· {versionLabel}</span>
1622
+ <span className="shrink-0 text-og-fg-subtle">· {versionLabel}</span>
1439
1623
  ) : null}
1440
- <ChevronDownIcon className="size-3 shrink-0 text-og-subtle" />
1624
+ <ChevronDownIcon className="size-3 shrink-0 text-og-fg-subtle" />
1441
1625
  </summary>
1442
- <div className="absolute right-0 top-8 z-40 w-72 rounded-og-md border border-og-border bg-og-surface-1 p-3 shadow-xl">
1626
+ <div className="absolute right-2 top-10 z-40 max-h-[calc(100%-3rem)] w-72 max-w-[calc(100%-1rem)] overflow-y-auto rounded-og-md border border-og-border bg-og-surface-1 p-3 shadow-xl">
1443
1627
  <div className="flex items-start gap-2">
1444
- <span className="grid size-7 shrink-0 place-items-center rounded-og-sm bg-og-surface-2 text-og-muted">
1628
+ <span className="grid size-7 shrink-0 place-items-center rounded-og-sm bg-og-surface-2 text-og-fg-muted">
1445
1629
  {attached ? (
1446
1630
  <MonitorIcon className="size-3.5" />
1447
1631
  ) : (
@@ -1452,7 +1636,7 @@ function BrowserProfileMenu(props: {
1452
1636
  <p className="truncate text-og-control font-medium text-og-fg">
1453
1637
  {attached ? props.session?.name : (props.identity?.name ?? "Temporary browser")}
1454
1638
  </p>
1455
- <p className="mt-0.5 text-og-xs leading-4 text-og-subtle">
1639
+ <p className="mt-0.5 text-og-xs leading-4 text-og-fg-subtle">
1456
1640
  {attached
1457
1641
  ? "This session drives the existing Chrome profile and its current login state."
1458
1642
  : props.identity?.status === "archived"
@@ -1467,7 +1651,7 @@ function BrowserProfileMenu(props: {
1467
1651
  </div>
1468
1652
  {props.identity && revisions.length > 0 ? (
1469
1653
  <div className="mt-3 border-t border-og-border pt-2">
1470
- <p className="px-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-subtle">
1654
+ <p className="px-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-fg-subtle">
1471
1655
  Saved versions
1472
1656
  </p>
1473
1657
  <div className="mt-1 max-h-40 overflow-y-auto">
@@ -1487,19 +1671,19 @@ function BrowserProfileMenu(props: {
1487
1671
  onClick={() => openVersion(props.identity!.id, revision.id)}
1488
1672
  className="flex min-w-0 flex-1 items-center gap-2 px-2 py-1.5 text-left disabled:opacity-50"
1489
1673
  >
1490
- <span className="grid size-4 place-items-center text-og-muted">
1674
+ <span className="grid size-4 place-items-center text-og-fg-muted">
1491
1675
  {isCurrent ? <CheckIcon className="size-3.5 text-og-accent" /> : null}
1492
1676
  </span>
1493
1677
  <span className="min-w-0 flex-1">
1494
1678
  <span className="block text-og-control text-og-fg">
1495
1679
  Version {revision.ordinal}
1496
1680
  </span>
1497
- <span className="block truncate text-[10px] text-og-subtle">
1681
+ <span className="block truncate text-[10px] text-og-fg-subtle">
1498
1682
  {materialization.label}
1499
1683
  </span>
1500
1684
  </span>
1501
1685
  {isCurrent ? (
1502
- <span className="text-[10px] text-og-subtle">Current</span>
1686
+ <span className="text-[10px] text-og-fg-subtle">Current</span>
1503
1687
  ) : null}
1504
1688
  </button>
1505
1689
  <button
@@ -1514,7 +1698,7 @@ function BrowserProfileMenu(props: {
1514
1698
  onClick={() =>
1515
1699
  void props.onUpdate(props.identity!, { defaultRevisionId: revision.id })
1516
1700
  }
1517
- className="mr-1 shrink-0 rounded-og-sm px-2 py-1 text-[10px] text-og-subtle transition hover:bg-og-surface-3 hover:text-og-fg disabled:cursor-default disabled:opacity-70"
1701
+ className="mr-1 shrink-0 rounded-og-sm px-2 py-1 text-[10px] text-og-fg-subtle transition hover:bg-og-surface-3 hover:text-og-fg disabled:cursor-default disabled:opacity-70"
1518
1702
  >
1519
1703
  {isDefault ? "Default" : "Make default"}
1520
1704
  </button>
@@ -1522,7 +1706,7 @@ function BrowserProfileMenu(props: {
1522
1706
  );
1523
1707
  })}
1524
1708
  </div>
1525
- <p className="mt-1 px-2 text-[10px] leading-4 text-og-subtle">
1709
+ <p className="mt-1 px-2 text-[10px] leading-4 text-og-fg-subtle">
1526
1710
  Changing the default affects only browsers opened later. Saved browser data can be
1527
1711
  copied; a website may still expire or re-verify its own session.
1528
1712
  </p>
@@ -1530,13 +1714,13 @@ function BrowserProfileMenu(props: {
1530
1714
  ) : props.identity &&
1531
1715
  props.history?.identityId === props.identity.id &&
1532
1716
  props.history.loading ? (
1533
- <p className="mt-3 flex items-center gap-1.5 border-t border-og-border px-2 pt-2 text-og-xs text-og-subtle">
1717
+ <p className="mt-3 flex items-center gap-1.5 border-t border-og-border px-2 pt-2 text-og-xs text-og-fg-subtle">
1534
1718
  <LoaderCircleIcon className="size-3 animate-spin" /> Loading saved versions…
1535
1719
  </p>
1536
1720
  ) : null}
1537
1721
  {props.identity && props.authConnections.length > 0 ? (
1538
1722
  <div className="mt-3 border-t border-og-border pt-2">
1539
- <p className="px-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-subtle">
1723
+ <p className="px-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-fg-subtle">
1540
1724
  Login health
1541
1725
  </p>
1542
1726
  <div className="mt-1 space-y-1">
@@ -1552,7 +1736,7 @@ function BrowserProfileMenu(props: {
1552
1736
  <span className="block truncate text-og-control text-og-fg">
1553
1737
  {connection.name}
1554
1738
  </span>
1555
- <span className="block truncate text-[10px] text-og-subtle">
1739
+ <span className="block truncate text-[10px] text-og-fg-subtle">
1556
1740
  {connection.accountLabel} · {health.label}
1557
1741
  </span>
1558
1742
  </span>
@@ -1585,7 +1769,7 @@ function BrowserProfileMenu(props: {
1585
1769
  if (name.trim()) void save(name);
1586
1770
  }}
1587
1771
  >
1588
- <label className="text-og-xs font-medium text-og-muted" htmlFor={nameInputId}>
1772
+ <label className="text-og-xs font-medium text-og-fg-muted" htmlFor={nameInputId}>
1589
1773
  Profile name
1590
1774
  </label>
1591
1775
  <div className="mt-1 flex gap-1.5">
@@ -1595,12 +1779,12 @@ function BrowserProfileMenu(props: {
1595
1779
  onInput={(event) => setName(event.currentTarget.value)}
1596
1780
  maxLength={200}
1597
1781
  placeholder="Work"
1598
- className="h-8 min-w-0 flex-1 rounded-og-sm border border-og-border bg-og-bg px-2 text-og-control text-og-fg placeholder:text-og-subtle focus:border-og-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent"
1782
+ className="h-8 min-w-0 flex-1 rounded-og-sm border border-og-border bg-og-bg px-2 text-og-control text-og-fg placeholder:text-og-fg-subtle focus:border-og-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent"
1599
1783
  />
1600
1784
  <button
1601
1785
  type="submit"
1602
1786
  disabled={!name.trim() || props.saving}
1603
- className="inline-flex h-8 items-center gap-1 rounded-og-sm bg-og-accent px-2.5 text-og-control font-medium text-white transition hover:opacity-90 disabled:opacity-40"
1787
+ className="inline-flex h-8 items-center gap-1 rounded-og-sm border border-og-primary-border bg-og-primary text-og-primary-fg px-2.5 text-og-control font-medium transition hover:bg-og-primary-hover disabled:opacity-50"
1604
1788
  >
1605
1789
  {props.saving ? (
1606
1790
  <LoaderCircleIcon className="size-3.5 animate-spin" />
@@ -1613,9 +1797,9 @@ function BrowserProfileMenu(props: {
1613
1797
  </form>
1614
1798
  )
1615
1799
  ) : (
1616
- <p className="mt-3 rounded-og-sm bg-og-surface-2 px-2.5 py-2 text-og-xs text-og-subtle">
1800
+ <p className="mt-3 rounded-og-sm bg-og-surface-2 px-2.5 py-2 text-og-xs text-og-fg-subtle">
1617
1801
  {attached
1618
- ? "Chrome keeps this profile's state directly; OpenGeni does not copy it automatically."
1802
+ ? "Chrome keeps this profile's state directly; Opengeni does not copy it automatically."
1619
1803
  : props.identity?.status === "archived"
1620
1804
  ? "Restore this profile before saving another version."
1621
1805
  : "This browser cannot save reusable profile state."}
@@ -1630,7 +1814,7 @@ function BrowserProfileMenu(props: {
1630
1814
  status: props.identity!.status === "active" ? "archived" : "active",
1631
1815
  })
1632
1816
  }
1633
- className="mt-2 flex h-8 w-full items-center justify-center gap-1.5 rounded-og-sm px-3 text-og-control text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg disabled:opacity-50"
1817
+ className="mt-2 flex h-8 w-full items-center justify-center gap-1.5 rounded-og-sm px-3 text-og-control text-og-fg-muted transition hover:bg-og-surface-2 hover:text-og-fg disabled:opacity-50"
1634
1818
  >
1635
1819
  {props.identity.status === "active" ? (
1636
1820
  <ArchiveIcon className="size-3.5" />
@@ -1641,8 +1825,9 @@ function BrowserProfileMenu(props: {
1641
1825
  </button>
1642
1826
  ) : null}
1643
1827
  {canSave ? (
1644
- <p className="mt-2 text-[10px] leading-4 text-og-subtle">
1645
- Saving briefly restarts this browser. Other open browsers are unchanged.
1828
+ <p className="mt-2 text-[10px] leading-4 text-og-fg-subtle">
1829
+ Saving restarts this browser. Save unfinished work on its pages first; unsaved form
1830
+ entries may be lost. Other open browsers are unchanged.
1646
1831
  </p>
1647
1832
  ) : null}
1648
1833
  </div>
@@ -1695,7 +1880,7 @@ function MenuButton(props: { children: ReactNode; onClick: () => void; disabled?
1695
1880
  type="button"
1696
1881
  onClick={props.onClick}
1697
1882
  disabled={props.disabled}
1698
- className="flex h-7 flex-1 items-center justify-center gap-1 rounded-og-sm px-2 text-og-control text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg disabled:opacity-50"
1883
+ className="flex h-7 flex-1 items-center justify-center gap-1 rounded-og-sm px-2 text-og-control text-og-fg-muted transition hover:bg-og-surface-2 hover:text-og-fg disabled:opacity-50"
1699
1884
  >
1700
1885
  {props.children}
1701
1886
  </button>
@@ -1713,7 +1898,7 @@ function BrowserTabs(props: {
1713
1898
  }) {
1714
1899
  const pages = props.targets.filter((target) => target.kind === "page" || target.kind === "popup");
1715
1900
  return (
1716
- <div className="flex h-9 shrink-0 items-end gap-px overflow-x-auto border-b border-og-border bg-og-surface-0 px-1 pt-1">
1901
+ <div className="flex h-9 shrink-0 items-end gap-px overflow-x-auto border-b border-og-border bg-og-surface-1 px-1 pt-1">
1717
1902
  {pages.map((target) => (
1718
1903
  <div
1719
1904
  key={target.id}
@@ -1721,7 +1906,7 @@ function BrowserTabs(props: {
1721
1906
  "group flex h-8 min-w-24 max-w-48 items-center rounded-t-og-sm border border-b-0 border-transparent px-2",
1722
1907
  target.id === props.selectedTargetId
1723
1908
  ? "border-og-border bg-og-bg text-og-fg"
1724
- : "text-og-muted hover:bg-og-surface-1",
1909
+ : "text-og-fg-muted hover:bg-og-surface-1",
1725
1910
  )}
1726
1911
  >
1727
1912
  <button
@@ -1749,7 +1934,7 @@ function BrowserTabs(props: {
1749
1934
  type="button"
1750
1935
  onClick={props.onOpen}
1751
1936
  disabled={props.mutating}
1752
- className="mb-1 grid size-6 shrink-0 place-items-center rounded-og-sm text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg disabled:opacity-40"
1937
+ className="mb-1 grid size-6 shrink-0 place-items-center rounded-og-sm text-og-fg-muted transition hover:bg-og-surface-2 hover:text-og-fg disabled:opacity-40"
1753
1938
  aria-label="New tab"
1754
1939
  >
1755
1940
  <PlusIcon className="size-3.5" />
@@ -1763,34 +1948,73 @@ function BrowserAddressBar(props: {
1763
1948
  target: BrowserTarget | null;
1764
1949
  loading: boolean;
1765
1950
  onNavigate: (url: string) => void;
1951
+ onHistory: (direction: "back" | "forward") => Promise<void>;
1766
1952
  onReload: () => void;
1767
1953
  }) {
1768
1954
  const [draft, setDraft] = useState(props.target?.url ?? "");
1769
1955
  const focusedRef = useRef(false);
1956
+ const historyPendingRef = useRef(false);
1957
+ const [historyPending, setHistoryPending] = useState(false);
1770
1958
  useEffect(() => {
1771
1959
  if (!focusedRef.current) setDraft(props.target?.url ?? "");
1772
1960
  }, [props.target?.id, props.target?.url]);
1773
1961
  const submit = (event: FormEvent) => {
1774
1962
  event.preventDefault();
1963
+ if (historyPendingRef.current) return;
1775
1964
  const normalized = normalizeBrowserAddress(draft);
1776
1965
  if (normalized) props.onNavigate(normalized);
1777
1966
  };
1967
+ const navigateHistory = async (direction: "back" | "forward") => {
1968
+ if (!props.target || props.loading || historyPendingRef.current) return;
1969
+ historyPendingRef.current = true;
1970
+ setHistoryPending(true);
1971
+ try {
1972
+ await props.onHistory(direction);
1973
+ } finally {
1974
+ historyPendingRef.current = false;
1975
+ setHistoryPending(false);
1976
+ }
1977
+ };
1778
1978
  return (
1779
1979
  <form
1780
1980
  onSubmit={submit}
1981
+ aria-busy={historyPending || undefined}
1781
1982
  className="flex h-10 shrink-0 items-center gap-1.5 border-b border-og-border bg-og-bg px-2"
1782
1983
  >
1984
+ {(["back", "forward"] as const).map((direction) => (
1985
+ <button
1986
+ key={direction}
1987
+ type="button"
1988
+ onClick={() => void navigateHistory(direction)}
1989
+ disabled={!props.target || props.loading}
1990
+ aria-disabled={historyPending || undefined}
1991
+ className={cn(
1992
+ "grid size-7 shrink-0 place-items-center rounded-og-sm text-og-fg-muted transition hover:bg-og-surface-2 hover:text-og-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent disabled:opacity-35",
1993
+ historyPending && "opacity-50",
1994
+ )}
1995
+ aria-label={direction === "back" ? "Back" : "Forward"}
1996
+ title={direction === "back" ? "Back" : "Forward"}
1997
+ >
1998
+ {direction === "back" ? (
1999
+ <ArrowLeftIcon className="size-3.5" />
2000
+ ) : (
2001
+ <ArrowRightIcon className="size-3.5" />
2002
+ )}
2003
+ </button>
2004
+ ))}
1783
2005
  <button
1784
2006
  type="button"
1785
2007
  onClick={props.onReload}
1786
- disabled={!props.target || props.loading}
1787
- className="grid size-7 place-items-center rounded-og-sm text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg disabled:opacity-35"
2008
+ disabled={!props.target || props.loading || historyPending}
2009
+ className="grid size-7 shrink-0 place-items-center rounded-og-sm text-og-fg-muted transition hover:bg-og-surface-2 hover:text-og-fg disabled:opacity-35"
1788
2010
  aria-label="Reload"
1789
2011
  >
1790
- <RefreshCwIcon className={cn("size-3.5", props.loading && "animate-spin")} />
2012
+ <RefreshCwIcon
2013
+ className={cn("size-3.5", (props.loading || historyPending) && "animate-spin")}
2014
+ />
1791
2015
  </button>
1792
- <div className="flex h-7 min-w-0 flex-1 items-center gap-2 rounded-og-sm border border-og-border bg-og-surface-0 px-2 focus-within:border-og-accent/60">
1793
- <Globe2Icon className="size-3 shrink-0 text-og-subtle" />
2016
+ <div className="flex h-7 min-w-0 flex-1 items-center gap-2 rounded-og-sm border border-og-border bg-og-surface-1 px-2 focus-within:border-og-accent/60">
2017
+ <Globe2Icon className="size-3 shrink-0 text-og-fg-subtle" />
1794
2018
  <input
1795
2019
  value={draft}
1796
2020
  onChange={(event) => setDraft(event.target.value)}
@@ -1803,11 +2027,12 @@ function BrowserAddressBar(props: {
1803
2027
  onKeyDown={(event) => {
1804
2028
  if (event.key !== "Enter") return;
1805
2029
  event.preventDefault();
2030
+ if (historyPendingRef.current) return;
1806
2031
  const normalized = normalizeBrowserAddress(draft);
1807
2032
  if (normalized) props.onNavigate(normalized);
1808
2033
  }}
1809
2034
  disabled={!props.target}
1810
- className="min-w-0 flex-1 bg-transparent text-og-control text-og-fg placeholder:text-og-subtle focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent disabled:opacity-50"
2035
+ className="min-w-0 flex-1 bg-transparent text-og-control text-og-fg placeholder:text-og-fg-subtle focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent disabled:opacity-50"
1811
2036
  aria-label="Address"
1812
2037
  spellCheck={false}
1813
2038
  placeholder="Search or enter address"
@@ -1818,16 +2043,35 @@ function BrowserAddressBar(props: {
1818
2043
  }
1819
2044
 
1820
2045
  function BrowserViewport(props: {
1821
- frame: BrowserFrame | null;
2046
+ focusHandoffRef: { current: string | null };
2047
+ focusScope: string;
2048
+ frame: BrowserViewportFrame | null;
1822
2049
  connectionState: string;
1823
2050
  supportsLiveFrames: boolean;
1824
2051
  connectionError: Error | null;
2052
+ controlUnavailable: boolean;
1825
2053
  observation: ReturnType<typeof useBrowserSession>["observation"];
1826
2054
  mutating: boolean;
1827
2055
  clipboardEnabled: boolean;
2056
+ inputBatchAttachment?: {
2057
+ browserSessionId: string;
2058
+ controllerGeneration: string;
2059
+ targetId: string;
2060
+ fencedInputBatches?: true | undefined;
2061
+ focusedInputObservations?: true | undefined;
2062
+ expiresAt: string;
2063
+ } | null;
1828
2064
  activityLabel?: string | undefined;
1829
- onAction: (action: BrowserAction, frame: BrowserFrame | null) => Promise<BrowserActionReceipt>;
2065
+ onAction: (
2066
+ action: BrowserAction | BrowserActionBatch,
2067
+ frame: BrowserFrameInputFence | null,
2068
+ ) => Promise<BrowserActionReceipt>;
1830
2069
  onReadClipboard: () => Promise<{ text: string }>;
2070
+ onObserveForInput: () => Promise<BrowserObservation>;
2071
+ onSelectFromObservation: (
2072
+ action: BrowserAction,
2073
+ observation: BrowserObservation,
2074
+ ) => Promise<BrowserActionReceipt>;
1831
2075
  onReconnect: () => void;
1832
2076
  reconnectLabel?: string | undefined;
1833
2077
  reconnectMessage?: string | undefined;
@@ -1835,39 +2079,71 @@ function BrowserViewport(props: {
1835
2079
  }) {
1836
2080
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
1837
2081
  const inputRef = useRef<HTMLTextAreaElement | null>(null);
2082
+ const [selectPopup, setSelectPopup] = useState<{
2083
+ observation: BrowserObservation | null;
2084
+ anchor: { x: number; y: number } | null;
2085
+ } | null>(null);
2086
+ const inputSequenceRef = useRef(0);
2087
+ const dismissSelectPopup = useCallback(() => {
2088
+ inputSequenceRef.current += 1;
2089
+ setSelectPopup({ observation: null, anchor: null });
2090
+ }, []);
1838
2091
  const composingRef = useRef(false);
1839
2092
  const pointerStartRef = useRef<PointerStart | null>(null);
1840
2093
  const lastClickRef = useRef<{
1841
2094
  at: number;
1842
2095
  x: number;
1843
2096
  y: number;
1844
- frame: BrowserFrame;
2097
+ frame: BrowserViewportFrame;
1845
2098
  } | null>(null);
1846
2099
  const wheelRef = useRef<{
1847
2100
  x: number;
1848
2101
  y: number;
1849
2102
  deltaX: number;
1850
2103
  deltaY: number;
1851
- frame: BrowserFrame;
2104
+ frame: BrowserViewportFrame;
1852
2105
  timer: ReturnType<typeof setTimeout> | null;
1853
2106
  } | null>(null);
1854
2107
  const pendingTextRef = useRef<{
1855
2108
  text: string;
1856
- frame: BrowserFrame | null;
2109
+ frame: BrowserViewportFrame | null;
1857
2110
  timer: ReturnType<typeof setTimeout>;
1858
2111
  } | null>(null);
1859
2112
  const actionRef = useRef(props.onAction);
1860
2113
  const readClipboardRef = useRef(props.onReadClipboard);
2114
+ const observeInputRef = useRef(props.onObserveForInput);
1861
2115
  const errorRef = useRef(props.onError);
1862
2116
  const actionTailRef = useRef<Promise<void>>(Promise.resolve());
1863
2117
  const actionQueueEpochRef = useRef(0);
1864
- const queuedFrameRef = useRef<BrowserFrame | null>(null);
2118
+ const queuedTypingRef = useRef<{
2119
+ actions: BrowserAction[];
2120
+ frame: BrowserFrameInputFence;
2121
+ epoch: number;
2122
+ } | null>(null);
2123
+ const queuedFrameRef = useRef<BrowserViewportFrame | null>(null);
2124
+ const currentFrameRef = useRef<BrowserViewportFrame | null>(props.frame);
2125
+ const paintedFrameRef = useRef<BrowserViewportFrame | null>(null);
2126
+ const [paintedFrame, setPaintedFrame] = useState<BrowserViewportFrame | null>(null);
1865
2127
  const decodingFrameRef = useRef(false);
1866
2128
  const mountedRef = useRef(true);
1867
2129
  actionRef.current = props.onAction;
1868
2130
  readClipboardRef.current = props.onReadClipboard;
2131
+ observeInputRef.current = props.onObserveForInput;
1869
2132
  errorRef.current = props.onError;
1870
2133
 
2134
+ const clearBufferedInput = useCallback(() => {
2135
+ dismissSelectPopup();
2136
+ if (wheelRef.current?.timer) clearTimeout(wheelRef.current.timer);
2137
+ if (pendingTextRef.current?.timer) clearTimeout(pendingTextRef.current.timer);
2138
+ wheelRef.current = null;
2139
+ pendingTextRef.current = null;
2140
+ queuedTypingRef.current = null;
2141
+ pointerStartRef.current = null;
2142
+ lastClickRef.current = null;
2143
+ composingRef.current = false;
2144
+ if (inputRef.current) inputRef.current.value = "";
2145
+ }, [dismissSelectPopup]);
2146
+
1871
2147
  const paintQueuedFrames = useCallback(() => {
1872
2148
  if (decodingFrameRef.current) return;
1873
2149
  decodingFrameRef.current = true;
@@ -1880,15 +2156,22 @@ function BrowserViewport(props: {
1880
2156
 
1881
2157
  let objectUrl: string | null = null;
1882
2158
  try {
1883
- const blob = new Blob([frame.data.slice().buffer], {
2159
+ const blob = new Blob([frame.data], {
1884
2160
  type: frame.mediaType,
1885
2161
  });
1886
2162
  if (typeof createImageBitmap === "function") {
1887
2163
  const bitmap = await createImageBitmap(blob);
1888
2164
  try {
1889
2165
  const canvas = canvasRef.current;
1890
- if (mountedRef.current && canvas) {
2166
+ if (
2167
+ mountedRef.current &&
2168
+ canvas &&
2169
+ currentFrameRef.current &&
2170
+ sameBrowserDocument(frame, currentFrameRef.current)
2171
+ ) {
1891
2172
  paintCanvas(canvas, bitmap, frame.width, frame.height);
2173
+ paintedFrameRef.current = frame;
2174
+ setPaintedFrame(frame);
1892
2175
  }
1893
2176
  } finally {
1894
2177
  bitmap.close();
@@ -1898,11 +2181,23 @@ function BrowserViewport(props: {
1898
2181
  objectUrl = URL.createObjectURL(blob);
1899
2182
  const image = await loadImage(objectUrl);
1900
2183
  const canvas = canvasRef.current;
1901
- if (mountedRef.current && canvas) {
2184
+ if (
2185
+ mountedRef.current &&
2186
+ canvas &&
2187
+ currentFrameRef.current &&
2188
+ sameBrowserDocument(frame, currentFrameRef.current)
2189
+ ) {
1902
2190
  paintCanvas(canvas, image, frame.width, frame.height);
2191
+ paintedFrameRef.current = frame;
2192
+ setPaintedFrame(frame);
1903
2193
  }
1904
2194
  } catch (cause) {
1905
- if (mountedRef.current) errorRef.current(cause);
2195
+ if (
2196
+ mountedRef.current &&
2197
+ currentFrameRef.current &&
2198
+ sameBrowserDocument(frame, currentFrameRef.current)
2199
+ )
2200
+ errorRef.current(cause);
1906
2201
  } finally {
1907
2202
  if (objectUrl) URL.revokeObjectURL(objectUrl);
1908
2203
  }
@@ -1914,53 +2209,135 @@ function BrowserViewport(props: {
1914
2209
  })();
1915
2210
  }, []);
1916
2211
 
1917
- useEffect(() => {
2212
+ useLayoutEffect(() => {
2213
+ const keyboardInput = inputRef.current;
2214
+ const focusHandoff = props.focusHandoffRef;
1918
2215
  mountedRef.current = true;
2216
+ if (focusHandoff.current === props.focusScope) {
2217
+ keyboardInput?.focus({ preventScroll: true });
2218
+ }
2219
+ focusHandoff.current = null;
1919
2220
  return () => {
2221
+ // Only a same-target replacement may inherit focus the keyboard sink owned.
2222
+ if (document.activeElement === keyboardInput) {
2223
+ focusHandoff.current = props.focusScope;
2224
+ }
1920
2225
  mountedRef.current = false;
2226
+ actionQueueEpochRef.current += 1;
1921
2227
  queuedFrameRef.current = null;
2228
+ clearBufferedInput();
1922
2229
  };
1923
- }, []);
2230
+ }, [clearBufferedInput, props.focusHandoffRef, props.focusScope]);
1924
2231
 
1925
- useEffect(() => {
1926
- if (!props.frame) return;
2232
+ useLayoutEffect(() => {
2233
+ if (currentFrameRef.current && !props.frame) {
2234
+ actionQueueEpochRef.current += 1;
2235
+ clearBufferedInput();
2236
+ }
2237
+ currentFrameRef.current = props.frame;
2238
+ if (!props.frame) {
2239
+ queuedFrameRef.current = null;
2240
+ paintedFrameRef.current = null;
2241
+ setPaintedFrame(null);
2242
+ return;
2243
+ }
1927
2244
  queuedFrameRef.current = props.frame;
1928
2245
  paintQueuedFrames();
1929
- }, [paintQueuedFrames, props.frame]);
1930
-
1931
- useEffect(
1932
- () => () => {
1933
- if (wheelRef.current?.timer) clearTimeout(wheelRef.current.timer);
1934
- if (pendingTextRef.current?.timer) clearTimeout(pendingTextRef.current.timer);
1935
- },
1936
- [],
1937
- );
2246
+ }, [clearBufferedInput, paintQueuedFrames, props.frame]);
1938
2247
 
1939
2248
  const enqueue = useCallback(
1940
2249
  (
1941
2250
  action: BrowserAction,
1942
- frame: BrowserFrame | null,
2251
+ frame: BrowserFrameInputFence | null,
1943
2252
  after?: (receipt: BrowserActionReceipt) => Promise<void>,
2253
+ selectAnchor?: { x: number; y: number },
1944
2254
  ) => {
2255
+ const inputSequence = ++inputSequenceRef.current;
2256
+ setSelectPopup({ observation: null, anchor: null });
2257
+ // Detach image bytes even when the caller supplied a complete painted frame.
2258
+ frame = frame
2259
+ ? {
2260
+ browserSessionId: frame.browserSessionId,
2261
+ controllerGeneration: frame.controllerGeneration,
2262
+ targetId: frame.targetId,
2263
+ targetGeneration: frame.targetGeneration,
2264
+ documentGeneration: frame.documentGeneration,
2265
+ frameId: frame.frameId,
2266
+ }
2267
+ : null;
1945
2268
  const epoch = actionQueueEpochRef.current;
2269
+ const attachment = props.inputBatchAttachment;
2270
+ const canBatchTyping =
2271
+ action.type === "type" &&
2272
+ !after &&
2273
+ frame &&
2274
+ attachment?.fencedInputBatches === true &&
2275
+ attachment.browserSessionId === frame.browserSessionId &&
2276
+ attachment.controllerGeneration === frame.controllerGeneration &&
2277
+ attachment.targetId === frame.targetId &&
2278
+ Date.parse(attachment.expiresAt) > Date.now();
2279
+ const previous = queuedTypingRef.current;
2280
+ if (
2281
+ canBatchTyping &&
2282
+ frame &&
2283
+ previous &&
2284
+ previous.epoch === epoch &&
2285
+ sameFrameFence(previous.frame, frame) &&
2286
+ previous.actions.length < 16
2287
+ ) {
2288
+ // Keep every existing text/input event boundary. Only share transport.
2289
+ previous.actions.push(action);
2290
+ return;
2291
+ }
2292
+ const typing = canBatchTyping && frame ? { actions: [action], frame, epoch } : null;
2293
+ // Every non-typing action is an ordering barrier, including keys and paste.
2294
+ queuedTypingRef.current = typing;
1946
2295
  actionTailRef.current = actionTailRef.current
1947
2296
  .catch(() => undefined)
1948
2297
  .then(async () => {
1949
- if (epoch !== actionQueueEpochRef.current) return;
1950
- const receipt = await actionRef.current(action, frame);
2298
+ if (!mountedRef.current || epoch !== actionQueueEpochRef.current) return;
2299
+ if (queuedTypingRef.current === typing) queuedTypingRef.current = null;
2300
+ const dispatchedAction =
2301
+ typing && typing.actions.length > 1
2302
+ ? { type: "batch" as const, actions: typing.actions, fenceEachAction: true as const }
2303
+ : action;
2304
+ // Read after the epoch guard: a queued render callback can retain every
2305
+ // frame from that render even when its input fence contains no bytes.
2306
+ const receipt = await actionRef.current(dispatchedAction, frame);
2307
+ if (!mountedRef.current || epoch !== actionQueueEpochRef.current) return;
2308
+ // Alt+Down explicitly opens a native dropdown from the keyboard.
2309
+ // Existing controllers return focused-input metadata for clicks only,
2310
+ // so use the ordinary authorized read for this explicit gesture.
2311
+ const keyboardSelect =
2312
+ dispatchedAction.type === "press" && dispatchedAction.key === "Alt+ArrowDown";
2313
+ if ((selectAnchor || keyboardSelect) && inputSequence === inputSequenceRef.current) {
2314
+ const observation = keyboardSelect
2315
+ ? await observeInputRef.current()
2316
+ : receipt.observation;
2317
+ if (
2318
+ mountedRef.current &&
2319
+ epoch === actionQueueEpochRef.current &&
2320
+ inputSequence === inputSequenceRef.current &&
2321
+ observation
2322
+ ) {
2323
+ setSelectPopup({ observation, anchor: selectAnchor ?? null });
2324
+ }
2325
+ }
1951
2326
  await after?.(receipt);
1952
2327
  })
1953
2328
  .catch((cause) => {
2329
+ if (!mountedRef.current || epoch !== actionQueueEpochRef.current) return;
1954
2330
  // Do not replay or continue input collected behind a failed request.
1955
2331
  actionQueueEpochRef.current += 1;
2332
+ clearBufferedInput();
1956
2333
  errorRef.current(cause);
1957
2334
  });
1958
2335
  },
1959
- [],
2336
+ [clearBufferedInput, props.inputBatchAttachment],
1960
2337
  );
1961
2338
 
1962
2339
  const point = useCallback(
1963
- (frame: BrowserFrame, clientX: number, clientY: number) =>
2340
+ (frame: BrowserViewportFrame, clientX: number, clientY: number) =>
1964
2341
  browserPoint(canvasRef.current, frame, clientX, clientY),
1965
2342
  [],
1966
2343
  );
@@ -1977,19 +2354,40 @@ function BrowserViewport(props: {
1977
2354
  lastClickRef.current = null;
1978
2355
  }, []);
1979
2356
 
2357
+ const flushPendingWheel = useCallback(() => {
2358
+ const batch = wheelRef.current;
2359
+ if (!batch) return;
2360
+ if (batch.timer) clearTimeout(batch.timer);
2361
+ wheelRef.current = null;
2362
+ enqueue(
2363
+ {
2364
+ type: "pointer",
2365
+ action: "scroll",
2366
+ x: batch.x,
2367
+ y: batch.y,
2368
+ deltaX: batch.deltaX,
2369
+ deltaY: batch.deltaY,
2370
+ },
2371
+ batch.frame,
2372
+ );
2373
+ }, [enqueue]);
2374
+
1980
2375
  const pointerDown = (event: PointerEvent<HTMLCanvasElement>) => {
1981
- if (!props.frame || event.button !== 0) return;
2376
+ const frame = paintedFrameRef.current;
2377
+ if (!frame || event.button !== 0) return;
1982
2378
  // Keep the hidden keyboard sink focused after the browser performs the
1983
2379
  // pointerdown default action for the canvas. Without preventing that
1984
2380
  // default, Chrome immediately moves focus back to the document and all
1985
2381
  // subsequent typing/paste is silently lost.
1986
2382
  event.preventDefault();
2383
+ dismissSelectPopup();
2384
+ flushPendingWheel();
1987
2385
  flushPendingText();
1988
2386
  pointerStartRef.current = {
1989
2387
  x: event.clientX,
1990
2388
  y: event.clientY,
1991
2389
  pointerId: event.pointerId,
1992
- frame: props.frame,
2390
+ frame,
1993
2391
  };
1994
2392
  event.currentTarget.setPointerCapture?.(event.pointerId);
1995
2393
  inputRef.current?.focus({ preventScroll: true });
@@ -2033,13 +2431,22 @@ function BrowserViewport(props: {
2033
2431
  return;
2034
2432
  }
2035
2433
  lastClickRef.current = { at: now, x: to.x, y: to.y, frame: start.frame };
2036
- enqueue({ type: "pointer", action: "click", x: to.x, y: to.y }, start.frame);
2434
+ const viewportBounds = canvasRef.current?.parentElement?.getBoundingClientRect();
2435
+ enqueue(
2436
+ { type: "pointer", action: "click", x: to.x, y: to.y },
2437
+ start.frame,
2438
+ undefined,
2439
+ viewportBounds
2440
+ ? { x: event.clientX - viewportBounds.left, y: event.clientY - viewportBounds.top + 8 }
2441
+ : undefined,
2442
+ );
2037
2443
  };
2038
2444
 
2039
2445
  const contextMenu = (event: MouseEvent<HTMLCanvasElement>) => {
2040
2446
  event.preventDefault();
2041
- const frame = props.frame;
2447
+ const frame = paintedFrameRef.current;
2042
2448
  if (!frame) return;
2449
+ flushPendingWheel();
2043
2450
  flushPendingText();
2044
2451
  flushPendingClick();
2045
2452
  const at = point(frame, event.clientX, event.clientY);
@@ -2047,41 +2454,47 @@ function BrowserViewport(props: {
2047
2454
  };
2048
2455
 
2049
2456
  const wheel = (event: WheelEvent<HTMLCanvasElement>) => {
2050
- const frame = props.frame;
2457
+ dismissSelectPopup();
2458
+ const frame = paintedFrameRef.current;
2051
2459
  if (!frame) return;
2052
2460
  const at = point(frame, event.clientX, event.clientY);
2053
2461
  if (!at) return;
2054
2462
  flushPendingText();
2055
2463
  flushPendingClick();
2056
2464
  event.preventDefault();
2057
- const pending = wheelRef.current;
2058
- if (pending?.timer) clearTimeout(pending.timer);
2465
+ let pending = wheelRef.current;
2466
+ if (
2467
+ pending &&
2468
+ (!sameBrowserDocument(pending.frame, frame) ||
2469
+ Math.hypot(pending.x - at.x, pending.y - at.y) > 6)
2470
+ ) {
2471
+ flushPendingWheel();
2472
+ pending = null;
2473
+ }
2059
2474
  wheelRef.current = {
2060
2475
  x: at.x,
2061
2476
  y: at.y,
2062
- deltaX: (pending && sameFrameFence(pending.frame, frame) ? pending.deltaX : 0) + event.deltaX,
2063
- deltaY: (pending && sameFrameFence(pending.frame, frame) ? pending.deltaY : 0) + event.deltaY,
2477
+ deltaX: (pending?.deltaX ?? 0) + event.deltaX,
2478
+ deltaY: (pending?.deltaY ?? 0) + event.deltaY,
2064
2479
  frame,
2065
- timer: setTimeout(() => {
2066
- const batch = wheelRef.current;
2067
- wheelRef.current = null;
2068
- if (batch)
2069
- enqueue(
2070
- {
2071
- type: "pointer",
2072
- action: "scroll",
2073
- x: batch.x,
2074
- y: batch.y,
2075
- deltaX: batch.deltaX,
2076
- deltaY: batch.deltaY,
2077
- },
2078
- batch.frame,
2079
- );
2080
- }, 45),
2480
+ // Retain the first event's deadline so a continuous gesture keeps moving.
2481
+ timer: pending?.timer ?? setTimeout(flushPendingWheel, 45),
2081
2482
  };
2082
2483
  };
2083
2484
 
2084
2485
  const keyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
2486
+ // Candidate selection/editing belongs to the local IME. Forward only the
2487
+ // committed text; Enter must not submit the remote page during composition.
2488
+ // Safari can end composition before keydown while retaining keyCode 229.
2489
+ if (
2490
+ composingRef.current ||
2491
+ event.nativeEvent.isComposing ||
2492
+ event.nativeEvent.keyCode === 229
2493
+ ) {
2494
+ return;
2495
+ }
2496
+ dismissSelectPopup();
2497
+ flushPendingWheel();
2085
2498
  const command = event.metaKey || event.ctrlKey;
2086
2499
  if (
2087
2500
  props.clipboardEnabled &&
@@ -2097,26 +2510,31 @@ function BrowserViewport(props: {
2097
2510
  event.preventDefault();
2098
2511
  flushPendingText();
2099
2512
  flushPendingClick();
2100
- enqueue({ type: "press", key }, props.frame);
2513
+ enqueue({ type: "press", key }, paintedFrameRef.current);
2101
2514
  };
2102
2515
 
2516
+ const finishCopy = useCallback(async () => {
2517
+ const clipboard = await readClipboardRef.current();
2518
+ if (!mountedRef.current) return;
2519
+ if (clipboard.text.length === 0) return;
2520
+ if (!(await copyTextToClipboard(clipboard.text))) {
2521
+ throw new Error("Browser text could not be copied to the local clipboard");
2522
+ }
2523
+ }, []);
2524
+
2103
2525
  const copy = (event: ClipboardEvent<HTMLTextAreaElement>) => {
2104
2526
  if (!props.clipboardEnabled) return;
2105
2527
  event.preventDefault();
2528
+ flushPendingWheel();
2106
2529
  flushPendingText();
2107
2530
  flushPendingClick();
2108
- enqueue({ type: "clipboard", operation: "copy" }, props.frame, async () => {
2109
- const clipboard = await readClipboardRef.current();
2110
- if (clipboard.text.length === 0) return;
2111
- if (!(await copyTextToClipboard(clipboard.text))) {
2112
- throw new Error("Browser text could not be copied to the local clipboard");
2113
- }
2114
- });
2531
+ enqueue({ type: "clipboard", operation: "copy" }, paintedFrameRef.current, finishCopy);
2115
2532
  };
2116
2533
 
2117
2534
  const paste = (event: ClipboardEvent<HTMLTextAreaElement>) => {
2118
2535
  if (!props.clipboardEnabled) return;
2119
2536
  event.preventDefault();
2537
+ flushPendingWheel();
2120
2538
  flushPendingText();
2121
2539
  flushPendingClick();
2122
2540
  enqueue(
@@ -2125,16 +2543,18 @@ function BrowserViewport(props: {
2125
2543
  operation: "paste",
2126
2544
  text: event.clipboardData.getData("text/plain"),
2127
2545
  },
2128
- props.frame,
2546
+ paintedFrameRef.current,
2129
2547
  );
2130
2548
  };
2131
2549
 
2132
2550
  const input = (value: string, nativeComposing = false) => {
2551
+ dismissSelectPopup();
2133
2552
  if (composingRef.current || nativeComposing) return;
2134
2553
  if (!value) return;
2554
+ flushPendingWheel();
2135
2555
  flushPendingClick();
2136
2556
  const pending = pendingTextRef.current;
2137
- if (pending && sameOptionalBrowserFrame(pending.frame, props.frame)) {
2557
+ if (pending && sameOptionalBrowserFrame(pending.frame, paintedFrameRef.current)) {
2138
2558
  clearTimeout(pending.timer);
2139
2559
  pending.text += value;
2140
2560
  pending.timer = setTimeout(flushPendingText, 16);
@@ -2142,7 +2562,7 @@ function BrowserViewport(props: {
2142
2562
  flushPendingText();
2143
2563
  pendingTextRef.current = {
2144
2564
  text: value,
2145
- frame: props.frame,
2565
+ frame: paintedFrameRef.current,
2146
2566
  timer: setTimeout(flushPendingText, 16),
2147
2567
  };
2148
2568
  }
@@ -2158,7 +2578,11 @@ function BrowserViewport(props: {
2158
2578
  input(event.currentTarget.value || event.data);
2159
2579
  };
2160
2580
 
2161
- const showCanvas = props.frame !== null;
2581
+ const showCanvas =
2582
+ !props.controlUnavailable &&
2583
+ props.frame !== null &&
2584
+ paintedFrame !== null &&
2585
+ sameBrowserDocument(props.frame, paintedFrame);
2162
2586
  return (
2163
2587
  <div className="relative min-h-0 flex-1 overflow-hidden bg-black">
2164
2588
  <canvas
@@ -2178,6 +2602,7 @@ function BrowserViewport(props: {
2178
2602
  />
2179
2603
  <textarea
2180
2604
  ref={inputRef}
2605
+ disabled={props.controlUnavailable}
2181
2606
  defaultValue=""
2182
2607
  onInput={(event) =>
2183
2608
  input(event.currentTarget.value, (event.nativeEvent as InputEvent).isComposing)
@@ -2189,6 +2614,7 @@ function BrowserViewport(props: {
2189
2614
  onPaste={paste}
2190
2615
  className="pointer-events-none absolute left-1/2 top-1/2 size-px resize-none overflow-hidden opacity-0"
2191
2616
  aria-label="Browser keyboard input"
2617
+ aria-keyshortcuts="Alt+ArrowDown"
2192
2618
  autoCapitalize="off"
2193
2619
  autoCorrect="off"
2194
2620
  spellCheck={false}
@@ -2199,12 +2625,28 @@ function BrowserViewport(props: {
2199
2625
  connectionState={props.connectionState}
2200
2626
  supportsLiveFrames={props.supportsLiveFrames}
2201
2627
  error={props.connectionError}
2628
+ controlUnavailable={props.controlUnavailable}
2202
2629
  onAction={(action) => enqueue(action, null)}
2203
2630
  onReconnect={props.onReconnect}
2204
2631
  reconnectLabel={props.reconnectLabel}
2205
2632
  reconnectMessage={props.reconnectMessage}
2206
2633
  />
2207
2634
  ) : null}
2635
+ {showCanvas ? (
2636
+ <BrowserSelectControl
2637
+ manualFallback={props.inputBatchAttachment?.focusedInputObservations !== true}
2638
+ activation={selectPopup}
2639
+ onDismiss={() => inputRef.current?.focus({ preventScroll: true })}
2640
+ observe={async () => {
2641
+ flushPendingText();
2642
+ flushPendingWheel();
2643
+ await actionTailRef.current;
2644
+ if (!mountedRef.current) throw new Error("The browser page changed.");
2645
+ return await props.onObserveForInput();
2646
+ }}
2647
+ act={props.onSelectFromObservation}
2648
+ />
2649
+ ) : null}
2208
2650
  {props.mutating ? (
2209
2651
  <div className="pointer-events-none absolute right-3 top-3 flex items-center gap-1.5 rounded-full border border-white/10 bg-black/65 px-2.5 py-1 text-[11px] text-white/80 backdrop-blur">
2210
2652
  <LoaderCircleIcon className="size-3 animate-spin" /> {props.activityLabel ?? "Acting"}
@@ -2219,47 +2661,52 @@ function SemanticBrowserFallback(props: {
2219
2661
  connectionState: string;
2220
2662
  supportsLiveFrames: boolean;
2221
2663
  error: Error | null;
2664
+ controlUnavailable: boolean;
2222
2665
  onAction: (action: BrowserAction) => void;
2223
2666
  onReconnect: () => void;
2224
2667
  reconnectLabel?: string | undefined;
2225
2668
  reconnectMessage?: string | undefined;
2226
2669
  }) {
2227
2670
  const controlFailure = interactionControlFailureFromError(props.error);
2228
- const generationLoss =
2229
- Boolean(props.reconnectMessage) || isAttachedChromeGenerationLossError(props.error);
2671
+ const { controlUnavailable } = props;
2672
+ const generationLoss = Boolean(props.reconnectMessage);
2230
2673
  const nodes = semanticNodes(
2231
2674
  props.observation?.semantic?.kind === "snapshot" ? props.observation.semantic.roots : [],
2232
2675
  );
2233
2676
  const interactive = nodes.filter((node) => node.actions.includes("click")).slice(0, 8);
2234
2677
  return (
2235
2678
  <div className="absolute inset-0 grid place-items-center bg-og-bg p-6">
2236
- <div className="w-full max-w-md rounded-og-lg border border-og-border bg-og-surface-0 p-4 shadow-lg">
2679
+ <div className="w-full max-w-md rounded-og-lg border border-og-border bg-og-surface-1 p-4 shadow-lg">
2237
2680
  <div className="flex items-center gap-2">
2238
2681
  {props.error ? (
2239
- <CircleAlertIcon className="size-4 text-og-status-error" />
2240
- ) : !props.supportsLiveFrames ? (
2241
- <ZapIcon className="size-4 text-og-muted" />
2682
+ <CircleAlertIcon className="size-4 text-og-danger" />
2683
+ ) : props.connectionState === "semantic" ? (
2684
+ <ZapIcon className="size-4 text-og-fg-muted" />
2242
2685
  ) : (
2243
- <LoaderCircleIcon className="size-4 animate-spin text-og-muted" />
2686
+ <LoaderCircleIcon className="size-4 animate-spin text-og-fg-muted" />
2244
2687
  )}
2245
2688
  <p className="text-og-menu font-medium text-og-fg">
2246
2689
  {generationLoss
2247
2690
  ? "Chrome reconnected—open a fresh browser/desktop."
2248
- : props.error
2249
- ? "Live view disconnected"
2250
- : props.supportsLiveFrames
2251
- ? browserConnectionLabel(props.connectionState)
2252
- : "Semantic browser"}
2691
+ : controlUnavailable
2692
+ ? "Browser controls unavailable"
2693
+ : props.error
2694
+ ? props.supportsLiveFrames
2695
+ ? "Live view disconnected"
2696
+ : "Browser unavailable"
2697
+ : props.connectionState === "semantic"
2698
+ ? "Semantic browser"
2699
+ : browserConnectionLabel(props.connectionState)}
2253
2700
  </p>
2254
2701
  </div>
2255
2702
  {props.error || props.reconnectMessage ? (
2256
- <p className="mt-2 text-og-control leading-5 text-og-muted">
2703
+ <p className="mt-2 text-og-control leading-5 text-og-fg-muted">
2257
2704
  {props.reconnectMessage ?? controlFailure?.message ?? props.error?.message}
2258
2705
  </p>
2259
2706
  ) : null}
2260
- {interactive.length > 0 ? (
2707
+ {interactive.length > 0 && !controlUnavailable ? (
2261
2708
  <div className="mt-3 border-t border-og-border pt-3">
2262
- <p className="mb-2 text-og-xs text-og-subtle">
2709
+ <p className="mb-2 text-og-xs text-og-fg-subtle">
2263
2710
  {props.supportsLiveFrames
2264
2711
  ? "Page controls remain available"
2265
2712
  : "Available page controls"}
@@ -2283,11 +2730,11 @@ function SemanticBrowserFallback(props: {
2283
2730
  </div>
2284
2731
  </div>
2285
2732
  ) : null}
2286
- {props.error && props.supportsLiveFrames && (!generationLoss || props.reconnectLabel) ? (
2733
+ {props.error && (!generationLoss || props.reconnectLabel) ? (
2287
2734
  <button
2288
2735
  type="button"
2289
2736
  onClick={props.onReconnect}
2290
- className="mt-3 text-og-control font-medium text-og-accent hover:underline"
2737
+ className="mt-3 text-og-control font-medium og-link"
2291
2738
  >
2292
2739
  {props.reconnectLabel ??
2293
2740
  (controlFailure?.retryable === false ? "Try again" : "Reconnect")}
@@ -2303,6 +2750,7 @@ function BrowserStatusBar(props: {
2303
2750
  profile: BrowserIdentity | null;
2304
2751
  target: BrowserTarget | null;
2305
2752
  observation: ReturnType<typeof useBrowserSession>["observation"];
2753
+ inputFailure: ReturnType<typeof useBrowserSession>["inputFailure"];
2306
2754
  connectionState: string;
2307
2755
  refreshing: boolean;
2308
2756
  diagnosticsOpen: boolean;
@@ -2314,7 +2762,7 @@ function BrowserStatusBar(props: {
2314
2762
  (props.observation?.diagnostics.pageErrorCount ?? 0);
2315
2763
  const failed = props.observation?.diagnostics.failedRequestCount ?? 0;
2316
2764
  return (
2317
- <div className="flex h-7 shrink-0 items-center gap-2 border-t border-og-border bg-og-surface-0 px-2 text-og-xs text-og-subtle">
2765
+ <div className="flex h-7 shrink-0 items-center gap-2 border-t border-og-border bg-og-surface-1 px-2 text-og-xs text-og-fg-subtle">
2318
2766
  <span
2319
2767
  className={cn(
2320
2768
  "size-1.5 rounded-full",
@@ -2330,6 +2778,11 @@ function BrowserStatusBar(props: {
2330
2778
  ? "Semantic"
2331
2779
  : browserConnectionLabel(props.connectionState)}
2332
2780
  </span>
2781
+ {props.inputFailure ? (
2782
+ <span className="text-og-status-failed">
2783
+ {props.inputFailure.state === "failed" ? "Input failed" : "Input result unknown"}
2784
+ </span>
2785
+ ) : null}
2333
2786
  <span>{props.profile?.name ?? "Temporary browser"}</span>
2334
2787
  <span className="min-w-0 flex-1 truncate">{props.target?.title}</span>
2335
2788
  {props.refreshing ? <LoaderCircleIcon className="size-3 animate-spin" /> : null}
@@ -2395,13 +2848,13 @@ function BrowserDiagnosticsDrawer(props: {
2395
2848
  id="browser-diagnostics-drawer"
2396
2849
  role="region"
2397
2850
  aria-label="Browser diagnostics"
2398
- className="absolute bottom-7 right-0 top-10 z-40 flex w-full max-w-md flex-col border-l border-og-border bg-og-surface-0 shadow-2xl"
2851
+ className="absolute inset-y-0 right-0 z-40 flex min-h-0 w-full max-w-md flex-col border-l border-og-border bg-og-surface-1 shadow-2xl"
2399
2852
  >
2400
- <div className="flex h-11 shrink-0 items-center gap-2 border-b border-og-border px-3">
2401
- <BugIcon className="size-4 text-og-muted" />
2853
+ <div className="flex min-h-11 shrink-0 items-center gap-2 border-b border-og-border px-3 py-2">
2854
+ <BugIcon className="size-4 shrink-0 text-og-fg-muted" />
2402
2855
  <div className="min-w-0 flex-1">
2403
2856
  <p className="text-og-control font-semibold text-og-fg">Browser diagnostics</p>
2404
- <p className="truncate text-og-xs text-og-subtle">
2857
+ <p className="truncate text-og-xs text-og-fg-subtle">
2405
2858
  {props.target?.title || shortUrl(props.target?.url ?? "") || "Current tab"}
2406
2859
  </p>
2407
2860
  </div>
@@ -2409,7 +2862,7 @@ function BrowserDiagnosticsDrawer(props: {
2409
2862
  type="button"
2410
2863
  onClick={props.onRefresh}
2411
2864
  disabled={props.state.loading}
2412
- className="grid size-7 place-items-center rounded-og-sm text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg disabled:opacity-40"
2865
+ className="grid size-7 shrink-0 place-items-center rounded-og-sm text-og-fg-muted transition hover:bg-og-surface-2 hover:text-og-fg disabled:opacity-40"
2413
2866
  aria-label="Refresh browser diagnostics"
2414
2867
  >
2415
2868
  <RefreshCwIcon className={cn("size-3.5", props.state.loading && "animate-spin")} />
@@ -2417,7 +2870,7 @@ function BrowserDiagnosticsDrawer(props: {
2417
2870
  <button
2418
2871
  type="button"
2419
2872
  onClick={props.onClose}
2420
- className="grid size-7 place-items-center rounded-og-sm text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg"
2873
+ className="grid size-7 shrink-0 place-items-center rounded-og-sm text-og-fg-muted transition hover:bg-og-surface-2 hover:text-og-fg"
2421
2874
  aria-label="Close browser diagnostics"
2422
2875
  >
2423
2876
  <XIcon className="size-3.5" />
@@ -2428,7 +2881,7 @@ function BrowserDiagnosticsDrawer(props: {
2428
2881
  <section className="border-b border-og-border p-3" aria-labelledby="browser-runtime-title">
2429
2882
  <h3
2430
2883
  id="browser-runtime-title"
2431
- className="text-[10px] font-semibold uppercase tracking-[0.14em] text-og-subtle"
2884
+ className="text-[10px] font-semibold uppercase tracking-[0.14em] text-og-fg-subtle"
2432
2885
  >
2433
2886
  Runtime
2434
2887
  </h3>
@@ -2459,18 +2912,19 @@ function BrowserDiagnosticsDrawer(props: {
2459
2912
  <section className="border-b border-og-border p-3" aria-labelledby="browser-page-title">
2460
2913
  <h3
2461
2914
  id="browser-page-title"
2462
- className="text-[10px] font-semibold uppercase tracking-[0.14em] text-og-subtle"
2915
+ className="text-[10px] font-semibold uppercase tracking-[0.14em] text-og-fg-subtle"
2463
2916
  >
2464
- Current page
2917
+ Tab diagnostics
2465
2918
  </h3>
2919
+ <p className="mt-1 text-og-xs text-og-fg-subtle">Includes earlier pages in this tab.</p>
2466
2920
  <dl className="mt-2 grid grid-cols-2 gap-2">
2467
- <DiagnosticCount label="Console errors" value={diagnostics?.consoleErrorCount ?? 0} />
2468
- <DiagnosticCount label="Page errors" value={diagnostics?.pageErrorCount ?? 0} />
2469
- <DiagnosticCount label="Failed requests" value={diagnostics?.failedRequestCount ?? 0} />
2470
- <DiagnosticCount label="Downloads" value={diagnostics?.downloadCount ?? 0} />
2921
+ <DiagnosticCount label="Console errors" value={diagnostics?.consoleErrorCount} />
2922
+ <DiagnosticCount label="Page errors" value={diagnostics?.pageErrorCount} />
2923
+ <DiagnosticCount label="Failed requests" value={diagnostics?.failedRequestCount} />
2924
+ <DiagnosticCount label="Downloads" value={diagnostics?.downloadCount} />
2471
2925
  </dl>
2472
2926
  {session?.failureCode ? (
2473
- <p className="mt-2 rounded-og-sm border border-og-status-error/30 bg-og-status-error/5 px-2.5 py-2 text-og-xs text-og-status-error">
2927
+ <p className="mt-2 rounded-og-sm border border-og-status-error/30 bg-og-status-error/5 px-2.5 py-2 text-og-xs text-og-danger">
2474
2928
  Browser state: {session.failureCode}
2475
2929
  </p>
2476
2930
  ) : null}
@@ -2492,25 +2946,25 @@ function BrowserDiagnosticsDrawer(props: {
2492
2946
  <div className="flex items-center justify-between gap-2">
2493
2947
  <h3
2494
2948
  id="browser-events-title"
2495
- className="text-[10px] font-semibold uppercase tracking-[0.14em] text-og-subtle"
2949
+ className="text-[10px] font-semibold uppercase tracking-[0.14em] text-og-fg-subtle"
2496
2950
  >
2497
2951
  Recent events
2498
2952
  </h3>
2499
2953
  {props.state.batch?.truncated ? (
2500
- <span className="text-[10px] text-og-subtle">Newest 100</span>
2954
+ <span className="text-[10px] text-og-fg-subtle">Newest 100</span>
2501
2955
  ) : null}
2502
2956
  </div>
2503
2957
  {props.state.loading && !props.state.batch ? (
2504
- <div className="flex items-center gap-2 py-6 text-og-control text-og-muted">
2958
+ <div className="flex items-center gap-2 py-6 text-og-control text-og-fg-muted">
2505
2959
  <LoaderCircleIcon className="size-3.5 animate-spin" /> Loading diagnostics…
2506
2960
  </div>
2507
2961
  ) : props.state.error ? (
2508
2962
  <div className="py-5">
2509
- <p className="text-og-control text-og-status-error">{props.state.error.message}</p>
2963
+ <p className="text-og-control text-og-danger">{props.state.error.message}</p>
2510
2964
  <button
2511
2965
  type="button"
2512
2966
  onClick={props.onRefresh}
2513
- className="mt-2 text-og-control font-medium text-og-accent hover:underline"
2967
+ className="mt-2 text-og-control font-medium og-link"
2514
2968
  >
2515
2969
  Try again
2516
2970
  </button>
@@ -2522,7 +2976,7 @@ function BrowserDiagnosticsDrawer(props: {
2522
2976
  key={entry.sequence}
2523
2977
  className="rounded-og-sm border border-og-border bg-og-bg p-2.5"
2524
2978
  >
2525
- <div className="flex items-center gap-2 text-[10px] text-og-subtle">
2979
+ <div className="flex items-center gap-2 text-[10px] text-og-fg-subtle">
2526
2980
  <span
2527
2981
  className={cn(
2528
2982
  "font-semibold uppercase tracking-wide",
@@ -2537,7 +2991,7 @@ function BrowserDiagnosticsDrawer(props: {
2537
2991
  {entry.message}
2538
2992
  </p>
2539
2993
  {entry.url || entry.filename || entry.method || entry.status ? (
2540
- <p className="mt-1 truncate font-og-mono text-[10px] text-og-subtle">
2994
+ <p className="mt-1 truncate font-og-mono text-[10px] text-og-fg-subtle">
2541
2995
  {[entry.method, entry.status, entry.filename, entry.url]
2542
2996
  .filter((value) => value !== null)
2543
2997
  .join(" · ")}
@@ -2547,7 +3001,7 @@ function BrowserDiagnosticsDrawer(props: {
2547
3001
  ))}
2548
3002
  </ol>
2549
3003
  ) : (
2550
- <p className="py-6 text-og-control text-og-muted">No diagnostics for this tab.</p>
3004
+ <p className="py-6 text-og-control text-og-fg-muted">No diagnostics for this tab.</p>
2551
3005
  )}
2552
3006
  </section>
2553
3007
  </div>
@@ -2594,35 +3048,35 @@ function BrowserDownloadsPanel(props: {
2594
3048
  return (
2595
3049
  <section className="border-b border-og-border p-3" aria-labelledby="browser-downloads-title">
2596
3050
  <div className="flex items-center gap-2">
2597
- <DownloadIcon className="size-3.5 text-og-muted" />
3051
+ <DownloadIcon className="size-3.5 text-og-fg-muted" />
2598
3052
  <h3
2599
3053
  id="browser-downloads-title"
2600
- className="text-[10px] font-semibold uppercase tracking-[0.14em] text-og-subtle"
3054
+ className="text-[10px] font-semibold uppercase tracking-[0.14em] text-og-fg-subtle"
2601
3055
  >
2602
3056
  Downloads
2603
3057
  </h3>
2604
3058
  {props.refreshing ? (
2605
- <LoaderCircleIcon className="ml-auto size-3 animate-spin text-og-muted" />
3059
+ <LoaderCircleIcon className="ml-auto size-3 animate-spin text-og-fg-muted" />
2606
3060
  ) : null}
2607
3061
  </div>
2608
3062
 
2609
3063
  {props.loading ? (
2610
- <div className="flex items-center gap-2 py-5 text-og-control text-og-muted">
3064
+ <div className="flex items-center gap-2 py-5 text-og-control text-og-fg-muted">
2611
3065
  <LoaderCircleIcon className="size-3.5 animate-spin" /> Loading downloads…
2612
3066
  </div>
2613
3067
  ) : props.error ? (
2614
3068
  <div className="py-4">
2615
- <p className="text-og-control text-og-status-error">{props.error.message}</p>
3069
+ <p className="text-og-control text-og-danger">{props.error.message}</p>
2616
3070
  <button
2617
3071
  type="button"
2618
3072
  onClick={props.onRefresh}
2619
- className="mt-2 text-og-control font-medium text-og-accent hover:underline"
3073
+ className="mt-2 text-og-control font-medium og-link"
2620
3074
  >
2621
3075
  Try again
2622
3076
  </button>
2623
3077
  </div>
2624
3078
  ) : props.downloads.length === 0 ? (
2625
- <p className="py-4 text-og-control text-og-muted">No files downloaded yet.</p>
3079
+ <p className="py-4 text-og-control text-og-fg-muted">No files downloaded yet.</p>
2626
3080
  ) : (
2627
3081
  <ol className="mt-2 space-y-2">
2628
3082
  {props.downloads.map((download) => {
@@ -2635,9 +3089,9 @@ function BrowserDownloadsPanel(props: {
2635
3089
  className="rounded-og-sm border border-og-border bg-og-bg p-2.5"
2636
3090
  >
2637
3091
  <div className="flex min-w-0 items-start gap-2">
2638
- <span className="mt-0.5 grid size-6 shrink-0 place-items-center rounded bg-og-surface-2 text-og-muted">
3092
+ <span className="mt-0.5 grid size-6 shrink-0 place-items-center rounded bg-og-surface-2 text-og-fg-muted">
2639
3093
  {isSaved ? (
2640
- <FileCheck2Icon className="size-3.5 text-og-status-success" />
3094
+ <FileCheck2Icon className="size-3.5 text-og-status-idle" />
2641
3095
  ) : (
2642
3096
  <DownloadIcon className="size-3.5" />
2643
3097
  )}
@@ -2649,7 +3103,7 @@ function BrowserDownloadsPanel(props: {
2649
3103
  >
2650
3104
  {download.filename}
2651
3105
  </p>
2652
- <p className="mt-0.5 text-[10px] text-og-subtle">
3106
+ <p className="mt-0.5 text-[10px] text-og-fg-subtle">
2653
3107
  {downloadStatusLabel(download)}
2654
3108
  </p>
2655
3109
  </div>
@@ -2671,7 +3125,7 @@ function BrowserDownloadsPanel(props: {
2671
3125
  }}
2672
3126
  disabled={isSaving}
2673
3127
  aria-label={`Workspace path for ${download.filename}`}
2674
- className="h-7 min-w-0 flex-1 rounded-og-sm border border-og-border bg-og-surface-0 px-2 font-og-mono text-[10px] text-og-fg transition focus:border-og-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent disabled:opacity-50"
3128
+ className="h-7 min-w-0 flex-1 rounded-og-sm border border-og-border bg-og-surface-1 px-2 font-og-mono text-[10px] text-og-fg transition focus:border-og-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent disabled:opacity-50"
2675
3129
  />
2676
3130
  <button
2677
3131
  type="button"
@@ -2697,7 +3151,7 @@ function BrowserDownloadsPanel(props: {
2697
3151
  : "Save"}
2698
3152
  </button>
2699
3153
  </div>
2700
- <label className="mt-1.5 flex w-fit items-center gap-1.5 text-[10px] text-og-subtle">
3154
+ <label className="mt-1.5 flex w-fit items-center gap-1.5 text-[10px] text-og-fg-subtle">
2701
3155
  <input
2702
3156
  type="checkbox"
2703
3157
  checked={overwrite[download.id] ?? false}
@@ -2714,7 +3168,7 @@ function BrowserDownloadsPanel(props: {
2714
3168
  Replace an existing workspace file
2715
3169
  </label>
2716
3170
  {errors[download.id] ? (
2717
- <p className="mt-1.5 break-words text-[10px] text-og-status-error">
3171
+ <p className="mt-1.5 break-words text-[10px] text-og-danger">
2718
3172
  {errors[download.id]}
2719
3173
  </p>
2720
3174
  ) : null}
@@ -2757,7 +3211,7 @@ function defaultDownloadDestination(download: BrowserDownload): string {
2757
3211
  function DiagnosticFact(props: { label: string; value: string }) {
2758
3212
  return (
2759
3213
  <>
2760
- <dt className="text-og-subtle">{props.label}</dt>
3214
+ <dt className="text-og-fg-subtle">{props.label}</dt>
2761
3215
  <dd className="min-w-0 truncate text-og-fg" title={props.value}>
2762
3216
  {props.value}
2763
3217
  </dd>
@@ -2765,17 +3219,17 @@ function DiagnosticFact(props: { label: string; value: string }) {
2765
3219
  );
2766
3220
  }
2767
3221
 
2768
- function DiagnosticCount(props: { label: string; value: number }) {
3222
+ function DiagnosticCount(props: { label: string; value: number | undefined }) {
2769
3223
  return (
2770
3224
  <div className="rounded-og-sm border border-og-border bg-og-bg px-2.5 py-2">
2771
- <dt className="text-[10px] text-og-subtle">{props.label}</dt>
3225
+ <dt className="text-[10px] text-og-fg-subtle">{props.label}</dt>
2772
3226
  <dd
2773
3227
  className={cn(
2774
3228
  "mt-0.5 text-og-sm font-semibold",
2775
- props.value ? "text-og-fg" : "text-og-muted",
3229
+ props.value ? "text-og-fg" : "text-og-fg-muted",
2776
3230
  )}
2777
3231
  >
2778
- {props.value}
3232
+ {props.value ?? "Unavailable"}
2779
3233
  </dd>
2780
3234
  </div>
2781
3235
  );
@@ -2806,9 +3260,9 @@ function diagnosticKindLabel(kind: BrowserDiagnosticBatch["entries"][number]["ki
2806
3260
  }
2807
3261
 
2808
3262
  function diagnosticTone(level: BrowserDiagnosticBatch["entries"][number]["level"]): string {
2809
- if (level === "error") return "text-og-status-error";
3263
+ if (level === "error") return "text-og-danger";
2810
3264
  if (level === "warning") return "text-og-status-waiting";
2811
- return "text-og-muted";
3265
+ return "text-og-fg-muted";
2812
3266
  }
2813
3267
 
2814
3268
  function formatDiagnosticTime(value: string): string {
@@ -2822,7 +3276,7 @@ function BrowserNotice(props: { icon: ReactNode; text: string; className?: strin
2822
3276
  return (
2823
3277
  <div
2824
3278
  className={cn(
2825
- "grid h-full place-items-center bg-og-bg text-og-control text-og-muted",
3279
+ "grid h-full place-items-center bg-og-bg text-og-control text-og-fg-muted",
2826
3280
  props.className,
2827
3281
  )}
2828
3282
  >
@@ -2858,7 +3312,7 @@ function loadImage(url: string): Promise<HTMLImageElement> {
2858
3312
 
2859
3313
  function browserPoint(
2860
3314
  canvas: HTMLCanvasElement | null,
2861
- frame: BrowserFrame | null,
3315
+ frame: BrowserViewportFrame | null,
2862
3316
  clientX: number,
2863
3317
  clientY: number,
2864
3318
  ): { x: number; y: number } | null {
@@ -2905,18 +3359,24 @@ function frameMatchesSelectedTarget(
2905
3359
  );
2906
3360
  }
2907
3361
 
2908
- function sameFrameFence(left: BrowserFrame, right: BrowserFrame): boolean {
3362
+ function sameFrameFence(left: BrowserFrameInputFence, right: BrowserFrameInputFence): boolean {
3363
+ return sameBrowserDocument(left, right) && left.frameId === right.frameId;
3364
+ }
3365
+
3366
+ function sameBrowserDocument(left: BrowserFrameInputFence, right: BrowserFrameInputFence): boolean {
2909
3367
  return (
2910
3368
  left.browserSessionId === right.browserSessionId &&
2911
3369
  left.controllerGeneration === right.controllerGeneration &&
2912
3370
  left.targetId === right.targetId &&
2913
3371
  left.targetGeneration === right.targetGeneration &&
2914
- left.documentGeneration === right.documentGeneration &&
2915
- left.frameId === right.frameId
3372
+ left.documentGeneration === right.documentGeneration
2916
3373
  );
2917
3374
  }
2918
3375
 
2919
- function sameOptionalBrowserFrame(left: BrowserFrame | null, right: BrowserFrame | null): boolean {
3376
+ function sameOptionalBrowserFrame(
3377
+ left: BrowserViewportFrame | null,
3378
+ right: BrowserViewportFrame | null,
3379
+ ): boolean {
2920
3380
  return left === null || right === null ? left === right : sameFrameFence(left, right);
2921
3381
  }
2922
3382