@opengeni/react 1.0.1 → 1.0.2-canary.37191796234001

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 (629) hide show
  1. package/README.md +651 -21
  2. package/dist/accounts.d.ts +64 -0
  3. package/dist/accounts.js +822 -0
  4. package/dist/accounts.js.map +1 -0
  5. package/dist/approvals.d.ts +2 -1
  6. package/dist/artifacts-document.js +3 -2
  7. package/dist/artifacts-presentation.js +3 -2
  8. package/dist/artifacts-spreadsheet.js +2 -2
  9. package/dist/artifacts.d.ts +4 -1
  10. package/dist/artifacts.js +518 -42
  11. package/dist/artifacts.js.map +1 -1
  12. package/dist/browser-viewer-GTR5XQ7X.js +16 -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-UGBTNB3L.js +11 -0
  16. package/dist/chat-interactive-block-UGBTNB3L.js.map +1 -0
  17. package/dist/{chunk-5UQPNLIO.js → chunk-23J2ZNXT.js} +4117 -3896
  18. package/dist/chunk-23J2ZNXT.js.map +1 -0
  19. package/dist/chunk-447JLCBU.js +209 -0
  20. package/dist/chunk-447JLCBU.js.map +1 -0
  21. package/dist/chunk-4RVYOY4O.js +379 -0
  22. package/dist/chunk-4RVYOY4O.js.map +1 -0
  23. package/dist/chunk-5CVXZA3Y.js +292 -0
  24. package/dist/chunk-5CVXZA3Y.js.map +1 -0
  25. package/dist/chunk-5J4FOW6Q.js +108 -0
  26. package/dist/chunk-5J4FOW6Q.js.map +1 -0
  27. package/dist/chunk-A2PI6UMY.js +41 -0
  28. package/dist/chunk-A2PI6UMY.js.map +1 -0
  29. package/dist/{chunk-7WCLAEFV.js → chunk-A4UJB2MM.js} +37 -27
  30. package/dist/chunk-A4UJB2MM.js.map +1 -0
  31. package/dist/chunk-B3V6TG77.js +41 -0
  32. package/dist/chunk-B3V6TG77.js.map +1 -0
  33. package/dist/chunk-BRM6FVZA.js +302 -0
  34. package/dist/chunk-BRM6FVZA.js.map +1 -0
  35. package/dist/chunk-C5X5AJXK.js +182 -0
  36. package/dist/chunk-C5X5AJXK.js.map +1 -0
  37. package/dist/chunk-CR7U7HAR.js +453 -0
  38. package/dist/chunk-CR7U7HAR.js.map +1 -0
  39. package/dist/{chunk-R3AZOAB5.js → chunk-DYWBUX5Q.js} +29 -123
  40. package/dist/chunk-DYWBUX5Q.js.map +1 -0
  41. package/dist/chunk-E3G7SAYZ.js +3124 -0
  42. package/dist/chunk-E3G7SAYZ.js.map +1 -0
  43. package/dist/chunk-ELCE5SXV.js +359 -0
  44. package/dist/chunk-ELCE5SXV.js.map +1 -0
  45. package/dist/chunk-ERGYJIBB.js +21 -0
  46. package/dist/chunk-ERGYJIBB.js.map +1 -0
  47. package/dist/chunk-FFA2OVOV.js +319 -0
  48. package/dist/chunk-FFA2OVOV.js.map +1 -0
  49. package/dist/chunk-FZCP527Y.js +116 -0
  50. package/dist/chunk-FZCP527Y.js.map +1 -0
  51. package/dist/chunk-GIMRELBW.js +14 -0
  52. package/dist/chunk-GIMRELBW.js.map +1 -0
  53. package/dist/{chunk-6KLX2RH5.js → chunk-H2NJQORO.js} +1069 -79
  54. package/dist/chunk-H2NJQORO.js.map +1 -0
  55. package/dist/chunk-I7IWRQLN.js +1608 -0
  56. package/dist/chunk-I7IWRQLN.js.map +1 -0
  57. package/dist/{chunk-HA46CYQK.js → chunk-IQUCRAT7.js} +792 -537
  58. package/dist/chunk-IQUCRAT7.js.map +1 -0
  59. package/dist/chunk-IWIDV7SB.js +1 -0
  60. package/dist/chunk-IWIDV7SB.js.map +1 -0
  61. package/dist/chunk-J2QWVSQY.js +312 -0
  62. package/dist/chunk-J2QWVSQY.js.map +1 -0
  63. package/dist/chunk-JFNEV4LU.js +2733 -0
  64. package/dist/chunk-JFNEV4LU.js.map +1 -0
  65. package/dist/chunk-JT4JW7BC.js +54 -0
  66. package/dist/chunk-JT4JW7BC.js.map +1 -0
  67. package/dist/chunk-K3HQAKQX.js +24 -0
  68. package/dist/chunk-K3HQAKQX.js.map +1 -0
  69. package/dist/{chunk-C5ZPSSS5.js → chunk-NEXLLTP4.js} +2 -1
  70. package/dist/{chunk-C5ZPSSS5.js.map → chunk-NEXLLTP4.js.map} +1 -1
  71. package/dist/chunk-NZUI76EM.js +371 -0
  72. package/dist/chunk-NZUI76EM.js.map +1 -0
  73. package/dist/{chunk-PEI6RANI.js → chunk-PNY3JKBJ.js} +309 -74
  74. package/dist/chunk-PNY3JKBJ.js.map +1 -0
  75. package/dist/{chunk-IGC6A2ZI.js → chunk-Q5QBEZIQ.js} +777 -246
  76. package/dist/chunk-Q5QBEZIQ.js.map +1 -0
  77. package/dist/chunk-RHREZ7BO.js +11 -0
  78. package/dist/chunk-RHREZ7BO.js.map +1 -0
  79. package/dist/chunk-SCOTPE2I.js +589 -0
  80. package/dist/chunk-SCOTPE2I.js.map +1 -0
  81. package/dist/{chunk-B6WPZSTT.js → chunk-SJZDKVW2.js} +1331 -1163
  82. package/dist/chunk-SJZDKVW2.js.map +1 -0
  83. package/dist/chunk-TMJRG4YC.js +12803 -0
  84. package/dist/chunk-TMJRG4YC.js.map +1 -0
  85. package/dist/chunk-TMYGL6X5.js +742 -0
  86. package/dist/chunk-TMYGL6X5.js.map +1 -0
  87. package/dist/chunk-UGLEYEMP.js +33 -0
  88. package/dist/chunk-UGLEYEMP.js.map +1 -0
  89. package/dist/chunk-UK4TINKT.js +27 -0
  90. package/dist/chunk-UK4TINKT.js.map +1 -0
  91. package/dist/chunk-UTRTG6LH.js +220 -0
  92. package/dist/chunk-UTRTG6LH.js.map +1 -0
  93. package/dist/chunk-WEHMPOXK.js +39 -0
  94. package/dist/chunk-WEHMPOXK.js.map +1 -0
  95. package/dist/chunk-WRHIZYZF.js +988 -0
  96. package/dist/chunk-WRHIZYZF.js.map +1 -0
  97. package/dist/{chunk-HUIKIDM5.js → chunk-WUISZ3WU.js} +223 -115
  98. package/dist/chunk-WUISZ3WU.js.map +1 -0
  99. package/dist/chunk-XCHWDJ22.js +107 -0
  100. package/dist/chunk-XCHWDJ22.js.map +1 -0
  101. package/dist/chunk-XOAMRDDK.js +481 -0
  102. package/dist/chunk-XOAMRDDK.js.map +1 -0
  103. package/dist/chunk-XUW2UWQM.js +299 -0
  104. package/dist/chunk-XUW2UWQM.js.map +1 -0
  105. package/dist/{chunk-KCGXPP5V.js → chunk-Y2PRMYLC.js} +11 -6
  106. package/dist/chunk-Y2PRMYLC.js.map +1 -0
  107. package/dist/chunk-YBXQDQFX.js +1772 -0
  108. package/dist/chunk-YBXQDQFX.js.map +1 -0
  109. package/dist/{chunk-KZ73RL76.js → chunk-YQEAA6LT.js} +7 -3
  110. package/dist/chunk-YQEAA6LT.js.map +1 -0
  111. package/dist/chunk-YV4L3ILF.js +9 -0
  112. package/dist/chunk-YV4L3ILF.js.map +1 -0
  113. package/dist/{chunk-LLOLUHVG.js → chunk-YWFG7ZX7.js} +1358 -2746
  114. package/dist/chunk-YWFG7ZX7.js.map +1 -0
  115. package/dist/chunk-ZYRGEKWS.js +216 -0
  116. package/dist/chunk-ZYRGEKWS.js.map +1 -0
  117. package/dist/client.d.ts +7 -4
  118. package/dist/clipboard.js +1 -0
  119. package/dist/commands/types.d.ts +2 -0
  120. package/dist/components/anthropic-mark.d.ts +3 -0
  121. package/dist/components/approval-surface.d.ts +15 -0
  122. package/dist/components/artifacts/artifact-chrome.d.ts +120 -0
  123. package/dist/components/artifacts/artifact-sandbox.d.ts +27 -0
  124. package/dist/components/artifacts/artifact-surface.d.ts +3 -1
  125. package/dist/components/artifacts/chat-interactive-block.d.ts +29 -0
  126. package/dist/components/artifacts/deferred-chat-media.d.ts +15 -0
  127. package/dist/components/artifacts/document-editor.d.ts +4 -2
  128. package/dist/components/artifacts/editable-artifact-ui.d.ts +2 -1
  129. package/dist/components/artifacts/editable-artifact-view.d.ts +46 -0
  130. package/dist/components/artifacts/editable-document.d.ts +1 -1
  131. package/dist/components/artifacts/editable-presentation.d.ts +2 -2
  132. package/dist/components/artifacts/editable-spreadsheet.d.ts +2 -1
  133. package/dist/components/artifacts/index.d.ts +8 -1
  134. package/dist/components/artifacts/inline-html-document.d.ts +2 -0
  135. package/dist/components/artifacts/latest-task-runner.d.ts +15 -0
  136. package/dist/components/artifacts/presentation-editor.d.ts +6 -2
  137. package/dist/components/artifacts/published-html-artifact-frame.d.ts +48 -3
  138. package/dist/components/artifacts/session-artifact-viewer.d.ts +39 -0
  139. package/dist/components/artifacts/site-view.d.ts +23 -0
  140. package/dist/components/artifacts/spreadsheet-general-display.d.ts +24 -0
  141. package/dist/components/artifacts/visualization-assets.gen.d.ts +2 -0
  142. package/dist/components/browser-select-control.d.ts +16 -0
  143. package/dist/components/browser-viewer.d.ts +10 -1
  144. package/dist/components/chat-composer.d.ts +21 -3
  145. package/dist/components/child-session-link.d.ts +5 -0
  146. package/dist/components/claude-mark.d.ts +3 -0
  147. package/dist/components/code-editor.d.ts +1 -4
  148. package/dist/components/composer.d.ts +45 -4
  149. package/dist/components/computer-viewer.d.ts +4 -1
  150. package/dist/components/copy-button.d.ts +3 -1
  151. package/dist/components/desktop-viewer.d.ts +1 -1
  152. package/dist/components/enrollment-consent.d.ts +6 -3
  153. package/dist/components/file-browser.d.ts +10 -1
  154. package/dist/components/fleet-tile.d.ts +2 -2
  155. package/dist/components/grok-mark.d.ts +26 -0
  156. package/dist/components/human-input-form.d.ts +21 -2
  157. package/dist/components/human-input-surface.d.ts +6 -3
  158. package/dist/components/machine-card.d.ts +4 -1
  159. package/dist/components/machines/machine-detail.d.ts +4 -1
  160. package/dist/components/machines/machine-operation-policy.d.ts +11 -0
  161. package/dist/components/machines-dashboard.d.ts +3 -1
  162. package/dist/components/markdown-table-layout.d.ts +12 -0
  163. package/dist/components/markdown.d.ts +47 -3
  164. package/dist/components/message-timeline.d.ts +72 -9
  165. package/dist/components/model-mark.d.ts +29 -0
  166. package/dist/components/model-policy-picker-menu.d.ts +9 -0
  167. package/dist/components/model-policy-picker.d.ts +62 -13
  168. package/dist/components/open-geni-chat.d.ts +55 -0
  169. package/dist/components/open-geni-links.d.ts +68 -0
  170. package/dist/components/openrouter-mark.d.ts +6 -0
  171. package/dist/components/pierre-file.d.ts +1 -1
  172. package/dist/components/preview-loading.d.ts +2 -0
  173. package/dist/components/queue-item-content.d.ts +9 -0
  174. package/dist/components/sandbox-files.d.ts +13 -2
  175. package/dist/components/sandbox-workspace.d.ts +43 -3
  176. package/dist/components/session-capability-frame.d.ts +35 -0
  177. package/dist/components/session-chrome.d.ts +66 -8
  178. package/dist/components/session-commands-panel.d.ts +6 -0
  179. package/dist/components/session-commands.d.ts +6 -0
  180. package/dist/components/session-connection-request.d.ts +8 -0
  181. package/dist/components/session-conversation.d.ts +86 -0
  182. package/dist/components/session-list.d.ts +36 -0
  183. package/dist/components/session-mcp-capability-card.d.ts +41 -0
  184. package/dist/components/timeline-anchor.d.ts +24 -0
  185. package/dist/components/timeline-annotation-chrome.d.ts +25 -0
  186. package/dist/components/timeline-annotation-layout.d.ts +42 -0
  187. package/dist/components/timeline-annotation-markers.d.ts +8 -0
  188. package/dist/components/timeline-annotation-reveal-context.d.ts +4 -0
  189. package/dist/components/timeline-annotation-shared.d.ts +31 -0
  190. package/dist/components/timeline-annotations-dialog.d.ts +3 -2
  191. package/dist/components/timeline-annotations.d.ts +13 -3
  192. package/dist/components/timeline-reading-progress.d.ts +11 -0
  193. package/dist/components/timeline-search.d.ts +34 -0
  194. package/dist/components/timeline-settlement.d.ts +16 -0
  195. package/dist/components/tip-follow.d.ts +16 -0
  196. package/dist/components/usage-limit-notice.d.ts +49 -0
  197. package/dist/components/usage-member-list.d.ts +91 -0
  198. package/dist/components/usage-meter.d.ts +73 -0
  199. package/dist/components/use-viewer-menu-dismiss.d.ts +2 -0
  200. package/dist/components/user-message-body.d.ts +10 -1
  201. package/dist/components/workspace-dock.d.ts +6 -2
  202. package/dist/composer.d.ts +5 -4
  203. package/dist/composer.js +15 -9
  204. package/dist/computer-viewer-ENHVAZ2Q.js +19 -0
  205. package/dist/computer-viewer-ENHVAZ2Q.js.map +1 -0
  206. package/dist/connect-account-identity.d.ts +8 -0
  207. package/dist/connect-accounts.d.ts +15 -0
  208. package/dist/connect-chooser.d.ts +18 -0
  209. package/dist/connect-panel.d.ts +18 -0
  210. package/dist/connect-setup.d.ts +16 -0
  211. package/dist/connect.d.ts +21 -0
  212. package/dist/connect.js +2129 -0
  213. package/dist/connect.js.map +1 -0
  214. package/dist/connection-catalog.d.ts +38 -0
  215. package/dist/connection-discovery.d.ts +12 -0
  216. package/dist/connection-installed.d.ts +17 -0
  217. package/dist/connection-logo.d.ts +9 -0
  218. package/dist/connection-service-logo.d.ts +8 -0
  219. package/dist/connection-service-presentation.d.ts +13 -0
  220. package/dist/connection-skeleton.d.ts +5 -0
  221. package/dist/connection-type-picker.d.ts +6 -0
  222. package/dist/conversation-timeline.d.ts +5 -0
  223. package/dist/desktop.d.ts +7 -0
  224. package/dist/desktop.js +26 -0
  225. package/dist/desktop.js.map +1 -0
  226. package/dist/device-authorization.d.ts +23 -0
  227. package/dist/diffs.d.ts +8 -0
  228. package/dist/diffs.js +14 -0
  229. package/dist/diffs.js.map +1 -0
  230. package/dist/discovery-cache.d.ts +5 -0
  231. package/dist/editor.d.ts +7 -0
  232. package/dist/editor.js +21 -0
  233. package/dist/editor.js.map +1 -0
  234. package/dist/embedded-session-client.d.ts +23 -0
  235. package/dist/file-node-visibility.d.ts +15 -0
  236. package/dist/{fleet-decision-row-GGCAT4E4.js → fleet-decision-row-YZ3ZKWMM.js} +2 -1
  237. package/dist/fleet-decision-row-YZ3ZKWMM.js.map +1 -0
  238. package/dist/hooks/channel-cache.d.ts +23 -0
  239. package/dist/hooks/latest-question.d.ts +25 -0
  240. package/dist/hooks/shared-machines-list.d.ts +14 -0
  241. package/dist/hooks/use-attached-browsers.d.ts +2 -1
  242. package/dist/hooks/use-browser-frame-stream.d.ts +1 -1
  243. package/dist/hooks/use-browser-session.d.ts +12 -1
  244. package/dist/hooks/use-channels.d.ts +1 -0
  245. package/dist/hooks/use-codex-accounts.d.ts +25 -7
  246. package/dist/hooks/use-composer.d.ts +45 -9
  247. package/dist/hooks/use-computer-frame-stream.d.ts +1 -1
  248. package/dist/hooks/use-computer-input-posture.d.ts +13 -0
  249. package/dist/hooks/use-computer-session.d.ts +2 -0
  250. package/dist/hooks/use-connect.d.ts +22 -0
  251. package/dist/hooks/use-desktop-stream.d.ts +1 -1
  252. package/dist/hooks/use-file-attachments.d.ts +21 -2
  253. package/dist/hooks/use-machine-chip.d.ts +3 -3
  254. package/dist/hooks/use-machines.d.ts +19 -1
  255. package/dist/hooks/use-sandbox-files.d.ts +5 -1
  256. package/dist/hooks/use-session-background-commands.d.ts +14 -0
  257. package/dist/hooks/use-session-events.d.ts +30 -8
  258. package/dist/hooks/use-session-lineage.d.ts +4 -0
  259. package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
  260. package/dist/hooks/use-session.d.ts +6 -0
  261. package/dist/hooks/use-turn-queue.d.ts +12 -0
  262. package/dist/hooks/use-usage.d.ts +40 -0
  263. package/dist/hooks/use-workspace-edit.d.ts +4 -0
  264. package/dist/hooks/use-workspace-sessions.d.ts +27 -5
  265. package/dist/human-input.d.ts +3 -1
  266. package/dist/identity-link-accounts.d.ts +11 -0
  267. package/dist/identity-link-consent.d.ts +18 -0
  268. package/dist/index.d.ts +60 -17
  269. package/dist/index.js +2237 -6200
  270. package/dist/index.js.map +1 -1
  271. package/dist/interaction.d.ts +1 -1
  272. package/dist/interaction.js +35 -184
  273. package/dist/interaction.js.map +1 -1
  274. package/dist/latest-question-755OW5DS.js +117 -0
  275. package/dist/latest-question-755OW5DS.js.map +1 -0
  276. package/dist/lib/error-message.d.ts +7 -0
  277. package/dist/lib/format.d.ts +36 -2
  278. package/dist/lib/host-theme.d.ts +70 -0
  279. package/dist/lib/interaction-errors.d.ts +6 -0
  280. package/dist/lib/menu-styles.d.ts +22 -0
  281. package/dist/lib/pierre-diffs-loader.d.ts +20 -0
  282. package/dist/lib/workbench-peers.d.ts +36 -0
  283. package/dist/lib/xterm-theme.d.ts +7 -5
  284. package/dist/machines.d.ts +4 -2
  285. package/dist/machines.js +14 -4
  286. package/dist/markdown-table-layout-QXSLK4BY.js +106 -0
  287. package/dist/markdown-table-layout-QXSLK4BY.js.map +1 -0
  288. package/dist/mcp-connection-status.d.ts +14 -0
  289. package/dist/model-policy-picker-menu-PWRI7R2X.js +14 -0
  290. package/dist/model-policy-picker-menu-PWRI7R2X.js.map +1 -0
  291. package/dist/model-policy.d.ts +19 -3
  292. package/dist/model-policy.js +9 -1
  293. package/dist/older-history.d.ts +30 -0
  294. package/dist/platform-activity-row-627BFZ46.js +182 -0
  295. package/dist/platform-activity-row-627BFZ46.js.map +1 -0
  296. package/dist/plugin-details.d.ts +14 -0
  297. package/dist/plugin-discovery.d.ts +25 -0
  298. package/dist/provider.d.ts +13 -1
  299. package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-ZHFI5JBP.js} +131 -44
  300. package/dist/queue-surface-implementation-ZHFI5JBP.js.map +1 -0
  301. package/dist/realtime/realtime-control.d.ts +6 -3
  302. package/dist/realtime.js +133 -74
  303. package/dist/realtime.js.map +1 -1
  304. package/dist/sandbox-files-BBC47SLD.js +20 -0
  305. package/dist/sandbox-files-BBC47SLD.js.map +1 -0
  306. package/dist/session-auth-recommendation.d.ts +5 -0
  307. package/dist/session-capability-policy.d.ts +12 -0
  308. package/dist/session-ui.d.ts +30 -2
  309. package/dist/session-ui.js +85 -7
  310. package/dist/session-ui.js.map +1 -1
  311. package/dist/session.d.ts +8 -2
  312. package/dist/session.js +30 -19
  313. package/dist/sites-ui.d.ts +34 -0
  314. package/dist/sites.d.ts +3 -0
  315. package/dist/sites.js +11 -0
  316. package/dist/sites.js.map +1 -0
  317. package/dist/skill-discovery.d.ts +47 -0
  318. package/dist/terminal.d.ts +8 -0
  319. package/dist/terminal.js +31 -0
  320. package/dist/terminal.js.map +1 -0
  321. package/dist/timeline/activity-rail.d.ts +11 -2
  322. package/dist/timeline/allowance-exhausted-row.d.ts +24 -0
  323. package/dist/timeline/compaction-copy.d.ts +10 -0
  324. package/dist/timeline/entrance.d.ts +13 -1
  325. package/dist/timeline/genie-loading.d.ts +34 -0
  326. package/dist/timeline/index.d.ts +6 -2
  327. package/dist/timeline/knowledge-receipt.d.ts +17 -0
  328. package/dist/timeline/platform-activity-row.d.ts +14 -0
  329. package/dist/timeline/presented-image.d.ts +6 -0
  330. package/dist/timeline/projection.d.ts +31 -2
  331. package/dist/timeline/registry.d.ts +2 -0
  332. package/dist/timeline/retained-image.d.ts +18 -0
  333. package/dist/timeline/rolling-activity.d.ts +11 -0
  334. package/dist/timeline/screenshot-lightbox.d.ts +16 -1
  335. package/dist/timeline/shared.d.ts +8 -3
  336. package/dist/timeline/startup-preference.d.ts +3 -0
  337. package/dist/timeline/startup-timings.d.ts +4 -0
  338. package/dist/timeline/tool-display-name.d.ts +2 -1
  339. package/dist/timeline/turn-summary.d.ts +32 -3
  340. package/dist/timeline/types.d.ts +141 -7
  341. package/dist/timeline-annotation-markers-GNDHTGBD.js +165 -0
  342. package/dist/timeline-annotation-markers-GNDHTGBD.js.map +1 -0
  343. package/dist/timeline-annotation-selection-DMLMM2TX.js +241 -0
  344. package/dist/timeline-annotation-selection-DMLMM2TX.js.map +1 -0
  345. package/dist/timeline-annotations-dialog-ELRDBHTS.js +273 -0
  346. package/dist/timeline-annotations-dialog-ELRDBHTS.js.map +1 -0
  347. package/dist/types/machines.d.ts +2 -2
  348. package/dist/usage/allowance-copy.d.ts +25 -0
  349. package/dist/usage/summary.d.ts +35 -0
  350. package/dist/usage.d.ts +13 -0
  351. package/dist/usage.js +1074 -0
  352. package/dist/usage.js.map +1 -0
  353. package/package.json +44 -4
  354. package/src/accounts.tsx +1072 -0
  355. package/src/approvals.ts +8 -1
  356. package/src/artifacts.ts +42 -0
  357. package/src/assets/README.md +8 -0
  358. package/src/assets/gmail.ico +0 -0
  359. package/src/assets/outlook.ico +0 -0
  360. package/src/capability-catalog-row.tsx +94 -0
  361. package/src/capability-logo-fallback.ts +22 -0
  362. package/src/client.ts +22 -13
  363. package/src/commands/registry.ts +18 -15
  364. package/src/commands/types.ts +2 -0
  365. package/src/components/anthropic-mark.tsx +10 -0
  366. package/src/components/approval-surface.tsx +171 -37
  367. package/src/components/artifacts/artifact-chrome.tsx +413 -0
  368. package/src/components/artifacts/artifact-sandbox.tsx +185 -0
  369. package/src/components/artifacts/artifact-surface.tsx +39 -19
  370. package/src/components/artifacts/chat-interactive-block.tsx +260 -0
  371. package/src/components/artifacts/deferred-chat-media.tsx +67 -0
  372. package/src/components/artifacts/document-editor.tsx +141 -16
  373. package/src/components/artifacts/editable-artifact-ui.tsx +4 -2
  374. package/src/components/artifacts/editable-artifact-view.tsx +187 -0
  375. package/src/components/artifacts/editable-artifact-workbench.tsx +5 -2
  376. package/src/components/artifacts/editable-document.tsx +37 -36
  377. package/src/components/artifacts/editable-presentation.tsx +36 -18
  378. package/src/components/artifacts/editable-spreadsheet.tsx +5 -2
  379. package/src/components/artifacts/index.ts +44 -0
  380. package/src/components/artifacts/inline-html-document.ts +24 -0
  381. package/src/components/artifacts/latest-task-runner.ts +49 -0
  382. package/src/components/artifacts/presentation-editor.tsx +323 -67
  383. package/src/components/artifacts/published-html-artifact-frame.tsx +338 -5
  384. package/src/components/artifacts/session-artifact-viewer.tsx +376 -0
  385. package/src/components/artifacts/site-view.tsx +116 -0
  386. package/src/components/artifacts/spreadsheet-canvas.ts +5 -10
  387. package/src/components/artifacts/spreadsheet-general-display.ts +43 -0
  388. package/src/components/artifacts/spreadsheet-grid.tsx +12 -13
  389. package/src/components/artifacts/visualization-assets/base.css +1015 -0
  390. package/src/components/artifacts/visualization-assets/helpers.html +349 -0
  391. package/src/components/artifacts/visualization-assets.gen.ts +3 -0
  392. package/src/components/browser-select-control.tsx +312 -0
  393. package/src/components/browser-viewer.tsx +1054 -314
  394. package/src/components/chat-composer.tsx +131 -85
  395. package/src/components/child-session-link.tsx +27 -0
  396. package/src/components/claude-mark.tsx +10 -0
  397. package/src/components/code-editor.tsx +13 -16
  398. package/src/components/composer-transcription-control.tsx +7 -5
  399. package/src/components/composer.tsx +376 -49
  400. package/src/components/computer-viewer.tsx +1041 -329
  401. package/src/components/copy-button.tsx +13 -5
  402. package/src/components/desktop-viewer.tsx +1 -1
  403. package/src/components/enrollment-consent.tsx +65 -7
  404. package/src/components/enrollment-device-flow.tsx +1 -1
  405. package/src/components/file-browser.tsx +158 -34
  406. package/src/components/fleet-tile.tsx +8 -16
  407. package/src/components/generated-video-player.tsx +4 -2
  408. package/src/components/grok-mark.tsx +33 -0
  409. package/src/components/human-input-form.tsx +455 -114
  410. package/src/components/human-input-surface.tsx +18 -7
  411. package/src/components/interaction-intervention-banner.tsx +2 -2
  412. package/src/components/machine-card.tsx +119 -4
  413. package/src/components/machine-dock-bar.tsx +1 -1
  414. package/src/components/machine-input-display.ts +23 -2
  415. package/src/components/machines/machine-detail.tsx +52 -2
  416. package/src/components/machines/machine-operation-policy.tsx +179 -0
  417. package/src/components/machines-dashboard.tsx +8 -17
  418. package/src/components/markdown-table-layout.ts +132 -0
  419. package/src/components/markdown.tsx +473 -44
  420. package/src/components/message-timeline.tsx +2498 -663
  421. package/src/components/model-mark.tsx +90 -0
  422. package/src/components/model-picker.tsx +15 -4
  423. package/src/components/model-policy-picker-menu.tsx +424 -0
  424. package/src/components/model-policy-picker.tsx +337 -381
  425. package/src/components/open-geni-chat.tsx +319 -0
  426. package/src/components/open-geni-links.tsx +179 -0
  427. package/src/components/openrouter-mark.tsx +13 -0
  428. package/src/components/pierre-diff.tsx +41 -16
  429. package/src/components/pierre-file.tsx +36 -11
  430. package/src/components/preview-loading.tsx +154 -0
  431. package/src/components/queue-item-content.ts +14 -0
  432. package/src/components/queue-surface-implementation.tsx +132 -42
  433. package/src/components/queue-surface-state.tsx +3 -1
  434. package/src/components/sandbox-files.tsx +267 -35
  435. package/src/components/sandbox-terminal.tsx +48 -11
  436. package/src/components/sandbox-workspace.tsx +337 -135
  437. package/src/components/session-capability-frame.tsx +192 -0
  438. package/src/components/session-chrome.tsx +1027 -328
  439. package/src/components/session-commands-panel.tsx +126 -0
  440. package/src/components/session-commands.tsx +18 -0
  441. package/src/components/session-connection-request.tsx +97 -0
  442. package/src/components/session-conversation.tsx +512 -0
  443. package/src/components/session-list.tsx +250 -0
  444. package/src/components/session-mcp-capability-card.tsx +680 -0
  445. package/src/components/session-status.tsx +1 -1
  446. package/src/components/timeline-anchor.tsx +119 -0
  447. package/src/components/timeline-annotation-chrome.tsx +196 -0
  448. package/src/components/timeline-annotation-layout.ts +268 -0
  449. package/src/components/timeline-annotation-markers.tsx +185 -0
  450. package/src/components/timeline-annotation-reveal-context.ts +9 -0
  451. package/src/components/timeline-annotation-selection.tsx +106 -54
  452. package/src/components/timeline-annotation-shared.ts +245 -0
  453. package/src/components/timeline-annotations-dialog.tsx +185 -128
  454. package/src/components/timeline-annotations.tsx +189 -45
  455. package/src/components/timeline-reading-progress.ts +80 -0
  456. package/src/components/timeline-search.tsx +168 -0
  457. package/src/components/timeline-settlement.ts +60 -0
  458. package/src/components/tip-follow.ts +70 -7
  459. package/src/components/tooltip.tsx +38 -5
  460. package/src/components/usage-limit-notice.tsx +179 -0
  461. package/src/components/usage-member-list.tsx +792 -0
  462. package/src/components/usage-meter.tsx +382 -0
  463. package/src/components/use-viewer-menu-dismiss.ts +33 -0
  464. package/src/components/user-message-body.tsx +194 -49
  465. package/src/components/workspace-dock.tsx +141 -175
  466. package/src/composer.ts +6 -1
  467. package/src/connect-account-identity.tsx +34 -0
  468. package/src/connect-accounts.tsx +233 -0
  469. package/src/connect-chooser.tsx +336 -0
  470. package/src/connect-panel.tsx +99 -0
  471. package/src/connect-setup.tsx +307 -0
  472. package/src/connect.ts +60 -0
  473. package/src/connection-assets.d.ts +4 -0
  474. package/src/connection-catalog.tsx +162 -0
  475. package/src/connection-discovery.tsx +235 -0
  476. package/src/connection-installed.tsx +51 -0
  477. package/src/connection-logo.tsx +37 -0
  478. package/src/connection-service-logo.tsx +55 -0
  479. package/src/connection-service-presentation.ts +46 -0
  480. package/src/connection-skeleton.tsx +16 -0
  481. package/src/connection-type-picker.tsx +36 -0
  482. package/src/conversation-timeline.ts +92 -0
  483. package/src/desktop.ts +24 -0
  484. package/src/device-authorization.tsx +98 -0
  485. package/src/diffs.ts +16 -0
  486. package/src/discovery-cache.ts +42 -0
  487. package/src/editor.ts +13 -0
  488. package/src/embedded-session-client.ts +104 -0
  489. package/src/file-node-visibility.ts +66 -0
  490. package/src/hooks/channel-cache.ts +104 -0
  491. package/src/hooks/internal.ts +3 -2
  492. package/src/hooks/latest-question.ts +164 -0
  493. package/src/hooks/shared-machines-list.ts +239 -0
  494. package/src/hooks/use-attached-browsers.ts +5 -1
  495. package/src/hooks/use-available-models.ts +20 -14
  496. package/src/hooks/use-browser-frame-stream.ts +25 -3
  497. package/src/hooks/use-browser-session.ts +271 -71
  498. package/src/hooks/use-browser-sessions.ts +7 -1
  499. package/src/hooks/use-channels.ts +32 -10
  500. package/src/hooks/use-codex-accounts.ts +78 -21
  501. package/src/hooks/use-composer.ts +1034 -320
  502. package/src/hooks/use-computer-frame-stream.ts +39 -16
  503. package/src/hooks/use-computer-input-posture.ts +94 -0
  504. package/src/hooks/use-computer-session.ts +110 -32
  505. package/src/hooks/use-computer-sessions.ts +8 -1
  506. package/src/hooks/use-connect.ts +23 -0
  507. package/src/hooks/use-desktop-stream.ts +19 -28
  508. package/src/hooks/use-file-attachments.ts +118 -10
  509. package/src/hooks/use-goal.ts +36 -9
  510. package/src/hooks/use-human-input.ts +31 -4
  511. package/src/hooks/use-last-started-turn-policy.ts +15 -13
  512. package/src/hooks/use-machine-chip.ts +23 -6
  513. package/src/hooks/use-machines.ts +104 -6
  514. package/src/hooks/use-sandbox-files.ts +253 -150
  515. package/src/hooks/use-sandbox-git.ts +58 -29
  516. package/src/hooks/use-session-background-commands.ts +67 -0
  517. package/src/hooks/use-session-capabilities.ts +21 -2
  518. package/src/hooks/use-session-events.ts +759 -433
  519. package/src/hooks/use-session-lineage.ts +28 -6
  520. package/src/hooks/use-session-mcp-approval-policy.ts +13 -5
  521. package/src/hooks/use-session.ts +75 -16
  522. package/src/hooks/use-slash-commands.ts +6 -11
  523. package/src/hooks/use-turn-queue.ts +113 -13
  524. package/src/hooks/use-usage.ts +100 -0
  525. package/src/hooks/use-voice-input.ts +12 -1
  526. package/src/hooks/use-workspace-capture.ts +3 -2
  527. package/src/hooks/use-workspace-edit.ts +23 -5
  528. package/src/hooks/use-workspace-sessions.ts +99 -11
  529. package/src/human-input.ts +12 -1
  530. package/src/identity-link-accounts.tsx +148 -0
  531. package/src/identity-link-consent.tsx +192 -0
  532. package/src/index.ts +157 -12
  533. package/src/interaction.ts +1 -0
  534. package/src/lib/error-message.ts +37 -0
  535. package/src/lib/format.ts +150 -14
  536. package/src/lib/host-theme.ts +511 -0
  537. package/src/lib/interaction-errors.ts +33 -0
  538. package/src/lib/menu-styles.ts +44 -0
  539. package/src/lib/pierre-diffs-loader.ts +53 -0
  540. package/src/lib/workbench-peers.ts +62 -0
  541. package/src/lib/xterm-theme.ts +23 -21
  542. package/src/machines.ts +9 -1
  543. package/src/mcp-connection-status.ts +35 -0
  544. package/src/model-policy.ts +223 -69
  545. package/src/older-history.ts +87 -0
  546. package/src/plugin-details.tsx +185 -0
  547. package/src/plugin-discovery.tsx +196 -0
  548. package/src/provider.tsx +55 -14
  549. package/src/realtime/dropdown-menu.tsx +19 -12
  550. package/src/realtime/realtime-control.tsx +133 -46
  551. package/src/session-auth-recommendation.ts +48 -0
  552. package/src/session-capability-policy.ts +83 -0
  553. package/src/session-context.ts +1 -0
  554. package/src/session-ui.ts +66 -2
  555. package/src/session.ts +18 -1
  556. package/src/sites-ui.tsx +402 -0
  557. package/src/sites.ts +3 -0
  558. package/src/skill-discovery.tsx +208 -0
  559. package/src/terminal.ts +26 -0
  560. package/src/timeline/activity-rail.tsx +171 -80
  561. package/src/timeline/allowance-exhausted-row.tsx +86 -0
  562. package/src/timeline/compaction-copy.ts +42 -0
  563. package/src/timeline/entrance.tsx +28 -3
  564. package/src/timeline/fleet-decision-projection.ts +2 -1
  565. package/src/timeline/fleet-decision-row.tsx +1 -0
  566. package/src/timeline/genie-loading.tsx +136 -0
  567. package/src/timeline/index.ts +11 -0
  568. package/src/timeline/knowledge-receipt.tsx +100 -0
  569. package/src/timeline/parsers.ts +2 -2
  570. package/src/timeline/platform-activity-row.tsx +216 -0
  571. package/src/timeline/presented-image.ts +49 -0
  572. package/src/timeline/projection.ts +1600 -84
  573. package/src/timeline/registry.ts +15 -3
  574. package/src/timeline/retained-image.ts +97 -0
  575. package/src/timeline/rolling-activity.tsx +92 -0
  576. package/src/timeline/screenshot-lightbox.tsx +115 -17
  577. package/src/timeline/shared.tsx +47 -15
  578. package/src/timeline/startup-preference.ts +33 -0
  579. package/src/timeline/startup-timings.tsx +145 -0
  580. package/src/timeline/tool-display-name.ts +8 -6
  581. package/src/timeline/tool-renderers.tsx +497 -128
  582. package/src/timeline/turn-summary.tsx +208 -44
  583. package/src/timeline/types.ts +186 -7
  584. package/src/types/machines.ts +10 -0
  585. package/src/usage/allowance-copy.ts +64 -0
  586. package/src/usage/summary.ts +93 -0
  587. package/src/usage.ts +50 -0
  588. package/styles/compiled.css +2098 -757
  589. package/styles/connect.css +284 -0
  590. package/styles/connect.d.ts +1 -0
  591. package/styles/effective-tokens.css +1 -0
  592. package/styles/index.css +201 -9
  593. package/styles/responsive.css +27 -1
  594. package/styles/session-capability.css +44 -0
  595. package/styles/tokens.css +135 -65
  596. package/dist/chunk-5UQPNLIO.js.map +0 -1
  597. package/dist/chunk-6KLX2RH5.js.map +0 -1
  598. package/dist/chunk-7WCLAEFV.js.map +0 -1
  599. package/dist/chunk-B6WPZSTT.js.map +0 -1
  600. package/dist/chunk-FK6VG4LL.js +0 -90
  601. package/dist/chunk-FK6VG4LL.js.map +0 -1
  602. package/dist/chunk-HA46CYQK.js.map +0 -1
  603. package/dist/chunk-HUIKIDM5.js.map +0 -1
  604. package/dist/chunk-IGC6A2ZI.js.map +0 -1
  605. package/dist/chunk-KCGXPP5V.js.map +0 -1
  606. package/dist/chunk-KZ73RL76.js.map +0 -1
  607. package/dist/chunk-LAKLF4PA.js +0 -141
  608. package/dist/chunk-LAKLF4PA.js.map +0 -1
  609. package/dist/chunk-LLOLUHVG.js.map +0 -1
  610. package/dist/chunk-MKPIAGVL.js +0 -465
  611. package/dist/chunk-MKPIAGVL.js.map +0 -1
  612. package/dist/chunk-PEI6RANI.js.map +0 -1
  613. package/dist/chunk-R3AZOAB5.js.map +0 -1
  614. package/dist/chunk-WSK7G5LE.js +0 -170
  615. package/dist/chunk-WSK7G5LE.js.map +0 -1
  616. package/dist/chunk-XBZWOM2K.js +0 -218
  617. package/dist/chunk-XBZWOM2K.js.map +0 -1
  618. package/dist/chunk-Y7OEDUJ5.js +0 -8810
  619. package/dist/chunk-Y7OEDUJ5.js.map +0 -1
  620. package/dist/fleet-decision-row-GGCAT4E4.js.map +0 -1
  621. package/dist/hooks/use-packs.d.ts +0 -32
  622. package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
  623. package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
  624. package/dist/timeline-annotation-selection-3L7LHGG2.js +0 -187
  625. package/dist/timeline-annotation-selection-3L7LHGG2.js.map +0 -1
  626. package/dist/timeline-annotations-dialog-U7EVLR75.js +0 -196
  627. package/dist/timeline-annotations-dialog-U7EVLR75.js.map +0 -1
  628. package/src/hooks/use-packs.ts +0 -157
  629. /package/dist/{interactive-workbench-viewers-TOHBXJEE.js.map → browser-viewer-GTR5XQ7X.js.map} +0 -0
@@ -1,6 +1,8 @@
1
1
  import type {
2
+ AttachedBrowserBridge,
2
3
  AttachedBrowserDevice,
3
4
  BrowserAction,
5
+ BrowserActionBatch,
4
6
  BrowserActionReceipt,
5
7
  BrowserDiagnosticBatch,
6
8
  BrowserDownload,
@@ -15,10 +17,14 @@ import type {
15
17
  InteractionPlacement,
16
18
  InteractionIntervention,
17
19
  InteractionSemanticNode,
20
+ SiteAuthConnection,
18
21
  } from "@opengeni/sdk/interaction";
22
+ import { interactionControlFailureFromError } from "@opengeni/sdk/interaction";
19
23
  import {
20
24
  BugIcon,
21
25
  ArchiveIcon,
26
+ ArrowLeftIcon,
27
+ ArrowRightIcon,
22
28
  CheckIcon,
23
29
  ChevronDownIcon,
24
30
  CircleAlertIcon,
@@ -47,6 +53,7 @@ import {
47
53
  useCallback,
48
54
  useEffect,
49
55
  useId,
56
+ useLayoutEffect,
50
57
  useMemo,
51
58
  useRef,
52
59
  useState,
@@ -58,15 +65,22 @@ import {
58
65
  import { useAttachedBrowsers } from "../hooks/use-attached-browsers";
59
66
  import { useBrowserIdentities } from "../hooks/use-browser-identities";
60
67
  import { useBrowserDownloads } from "../hooks/use-browser-downloads";
61
- import { useBrowserSession } from "../hooks/use-browser-session";
68
+ import { useBrowserSession, type BrowserFrameInputFence } from "../hooks/use-browser-session";
62
69
  import { useBrowserSessions } from "../hooks/use-browser-sessions";
63
70
  import { useInteractionInterventions } from "../hooks/use-interaction-interventions";
71
+ import { useSiteAuthConnections } from "../hooks/use-site-auth-connections";
64
72
  import { cn } from "../lib/cn";
65
73
  import { copyTextToClipboard } from "../lib/clipboard";
66
74
  import { formatBytes } from "../lib/format";
75
+ import {
76
+ isInteractionControlUnavailable,
77
+ isSourcePlacementChangedError,
78
+ } from "../lib/interaction-errors";
67
79
  import type { EmbeddedBrowserInteractionClientOverride } from "../session-context";
68
80
  import { browserKey, HUMAN_BROWSER_HOME_URL, normalizeBrowserAddress } from "./browser-input";
69
81
  import { InteractionInterventionBanner } from "./interaction-intervention-banner";
82
+ import { BrowserSelectControl } from "./browser-select-control";
83
+ import { useViewerMenuDismiss } from "./use-viewer-menu-dismiss";
70
84
 
71
85
  export type BrowserViewerNotification = {
72
86
  kind: "error" | "info";
@@ -77,6 +91,9 @@ export type BrowserViewerProps = EmbeddedBrowserInteractionClientOverride & {
77
91
  /** The selected OpenGeni agent/session. Peer BrowserSessions stay discoverable. */
78
92
  sessionId: string;
79
93
  enabled?: boolean | undefined;
94
+ /** Whether the Browser tab is selected. Keeps the mounted media stream warm
95
+ * while pausing semantic reads behind other dock tabs. */
96
+ active?: boolean | undefined;
80
97
  className?: string | undefined;
81
98
  onNotify?: ((notification: BrowserViewerNotification) => void) | undefined;
82
99
  /** Tests/demos only. Production uses the browser's native WebSocket. */
@@ -93,18 +110,28 @@ export type BrowserViewerProps = EmbeddedBrowserInteractionClientOverride & {
93
110
  | undefined;
94
111
  /** Navigate to the exact linked ComputerSession; never a lookalike desktop. */
95
112
  onOpenComputer?: ((computerSessionId: string) => void) | undefined;
113
+ /** Restore and report the BrowserSession selected in this task's dock. */
114
+ initialBrowserSessionId?: string | null | undefined;
115
+ onBrowserSessionIdChange?: ((browserSessionId: string | null) => void) | undefined;
116
+ /** Host-owned install/setup surface for attaching an existing Chrome profile.
117
+ * The machine bridge remains discoverable even when this URL is omitted. */
118
+ browserExtensionSetupUrl?: string | undefined;
96
119
  };
97
120
 
98
121
  type BrowserSelection = { sessionId: string; pinned: boolean } | null;
99
122
  type BrowserLaunchChoice =
100
123
  | { kind: "clean" }
101
- | { kind: "profile"; identityId: string }
124
+ | { kind: "profile"; identityId: string; baseRevisionId?: string }
102
125
  | { kind: "attached"; device: AttachedBrowserDevice };
126
+ // ArrayBuffer is opaque to React development prop diagnostics; a Uint8Array
127
+ // exposes every image byte as an enumerable property retained in timing entries.
128
+ type BrowserViewportFrame = Omit<BrowserFrame, "data"> & { data: ArrayBuffer };
129
+
103
130
  type PointerStart = {
104
131
  x: number;
105
132
  y: number;
106
133
  pointerId: number;
107
- frame: BrowserFrame;
134
+ frame: BrowserViewportFrame;
108
135
  };
109
136
  type BrowserResumeAttempt = {
110
137
  operationId: string;
@@ -128,32 +155,78 @@ type BrowserDiagnosticsView = {
128
155
  export function BrowserViewer({
129
156
  sessionId,
130
157
  enabled = true,
158
+ active = true,
131
159
  className,
132
160
  onNotify,
133
161
  webSocketFactory,
134
162
  renderEmpty,
135
163
  createLinkedComputer,
136
164
  onOpenComputer,
165
+ initialBrowserSessionId,
166
+ onBrowserSessionIdChange,
167
+ browserExtensionSetupUrl,
137
168
  ...override
138
169
  }: BrowserViewerProps) {
139
170
  const registry = useBrowserSessions({ ...override, sessionId, enabled });
140
171
  const attached = useAttachedBrowsers({ ...override, enabled });
141
- const profiles = useBrowserIdentities({ ...override, enabled, includeArchived: true });
142
- const activeProfiles = useMemo(
143
- () => profiles.identities.filter((identity) => identity.status === "active"),
144
- [profiles.identities],
172
+ const onlineAttachedBridges = useMemo(
173
+ () => attached.bridges.filter((bridge) => bridge.state === "online"),
174
+ [attached.bridges],
145
175
  );
176
+ const profiles = useBrowserIdentities({ ...override, enabled, includeArchived: true });
177
+ const siteAuth = useSiteAuthConnections({
178
+ ...override,
179
+ enabled: enabled && profiles.identities.length > 0,
180
+ });
146
181
  const createRegistryBrowser = registry.create;
182
+ const refreshRegistry = registry.refresh;
147
183
  const loadProfileRevisions = profiles.revisions;
148
184
  const liveSessions = useMemo(
149
185
  () => registry.sessions.filter((session) => isLiveBrowser(session)),
150
186
  [registry.sessions],
151
187
  );
188
+ const attachedGenerationLoss = useMemo(
189
+ () => attachedChromeGenerationLoss(registry.sessions),
190
+ [registry.sessions],
191
+ );
192
+ const endedGenerationLossRef = useRef(new Set<string>());
193
+ const endStaleBrowser = registry.end;
194
+ useEffect(() => {
195
+ if (!enabled) return;
196
+ for (const session of registry.sessions) {
197
+ if (
198
+ session.lifecycle !== "lost" ||
199
+ session.failureCode !== "controller_transition_expired" ||
200
+ session.placement.kind !== "attached_device"
201
+ ) {
202
+ continue;
203
+ }
204
+ if (endedGenerationLossRef.current.has(session.id)) continue;
205
+ endedGenerationLossRef.current.add(session.id);
206
+ void endStaleBrowser(session.id).catch(() => {
207
+ endedGenerationLossRef.current.delete(session.id);
208
+ });
209
+ }
210
+ }, [enabled, endStaleBrowser, registry.sessions]);
152
211
  const relevant = useMemo(
153
212
  () => registry.relevantSessions.filter((session) => isLiveBrowser(session)),
154
213
  [registry.relevantSessions],
155
214
  );
156
- const [selection, setSelection] = useState<BrowserSelection>(null);
215
+ const latestRelevantBrowser = registry.relevantSessions[0];
216
+ const unavailableBrowser =
217
+ latestRelevantBrowser?.lifecycle === "lost" || latestRelevantBrowser?.lifecycle === "failed"
218
+ ? latestRelevantBrowser
219
+ : undefined;
220
+ const [selection, setSelection] = useState<BrowserSelection>(() =>
221
+ initialBrowserSessionId ? { sessionId: initialBrowserSessionId, pinned: true } : null,
222
+ );
223
+ const selectBrowserSession = useCallback(
224
+ (next: BrowserSelection) => {
225
+ setSelection(next);
226
+ onBrowserSessionIdChange?.(next?.sessionId ?? null);
227
+ },
228
+ [onBrowserSessionIdChange],
229
+ );
157
230
  const interventions = useInteractionInterventions({
158
231
  ...override,
159
232
  enabled,
@@ -175,6 +248,7 @@ export function BrowserViewer({
175
248
  const previousSessionIdRef = useRef(sessionId);
176
249
  const seenInterventionIdsRef = useRef(new Set<string>());
177
250
  const diagnosticRequestRef = useRef(0);
251
+ const viewportFocusHandoffRef = useRef<string | null>(null);
178
252
  const [diagnosticsView, setDiagnosticsView] = useState<BrowserDiagnosticsView | null>(null);
179
253
 
180
254
  const notifyError = useCallback(
@@ -188,11 +262,14 @@ export function BrowserViewer({
188
262
  useEffect(() => {
189
263
  if (previousSessionIdRef.current !== sessionId) {
190
264
  previousSessionIdRef.current = sessionId;
191
- setSelection(null);
265
+ setSelection(
266
+ initialBrowserSessionId ? { sessionId: initialBrowserSessionId, pinned: true } : null,
267
+ );
192
268
  }
193
- }, [sessionId]);
269
+ }, [initialBrowserSessionId, sessionId]);
194
270
 
195
271
  useEffect(() => {
272
+ if (!enabled || registry.loading) return;
196
273
  const selectedStillLive = liveSessions.some((session) => session.id === selection?.sessionId);
197
274
  // A manually selected workspace browser stays selected, but an unrelated
198
275
  // browser must never become the implicit browser for this agent. That made
@@ -200,15 +277,15 @@ export function BrowserViewer({
200
277
  if (selection?.pinned && selectedStillLive) return;
201
278
  const preferred = relevant[0] ?? null;
202
279
  if (!preferred) {
203
- if (selection) setSelection(null);
280
+ if (selection) selectBrowserSession(null);
204
281
  return;
205
282
  }
206
283
  if (!selectedStillLive || !selection?.pinned) {
207
284
  if (selection?.sessionId !== preferred.id || selection.pinned) {
208
- setSelection({ sessionId: preferred.id, pinned: false });
285
+ selectBrowserSession({ sessionId: preferred.id, pinned: false });
209
286
  }
210
287
  }
211
- }, [liveSessions, relevant, selection]);
288
+ }, [enabled, liveSessions, registry.loading, relevant, selectBrowserSession, selection]);
212
289
 
213
290
  const selectedRegistrySession = useMemo(
214
291
  () => liveSessions.find((session) => session.id === selection?.sessionId) ?? null,
@@ -227,6 +304,7 @@ export function BrowserViewer({
227
304
  );
228
305
  const controllerReady = selectedRegistrySession?.lifecycle === "active";
229
306
  const resumeSession = registry.resume;
307
+ const [hasLiveFrame, setHasLiveFrame] = useState(false);
230
308
 
231
309
  const wakeBrowser = useCallback(
232
310
  async (session: BrowserSession, retry = false): Promise<void> => {
@@ -277,18 +355,19 @@ export function BrowserViewer({
277
355
  setResumeAttempt((visible) =>
278
356
  visible?.sessionId === selectedRegistrySession.id ? null : visible,
279
357
  );
280
- return;
281
- }
282
- if (selectedRegistrySession.lifecycle === "suspended") {
283
- void wakeBrowser(selectedRegistrySession);
284
358
  }
285
- }, [selectedRegistrySession, wakeBrowser]);
359
+ }, [selectedRegistrySession]);
286
360
 
287
361
  const browser = useBrowserSession({
288
362
  ...override,
289
363
  browserSessionId: selection?.sessionId ?? null,
290
364
  enabled: enabled && selection !== null && controllerReady,
365
+ semanticObservationEnabled: active && !hasLiveFrame,
291
366
  });
367
+ useEffect(() => {
368
+ if (!isSourcePlacementChangedError(browser.error, "browser_session")) return;
369
+ void refreshRegistry();
370
+ }, [browser.error, refreshRegistry]);
292
371
  const downloads = useBrowserDownloads({
293
372
  ...override,
294
373
  browserSessionId: selection?.sessionId ?? null,
@@ -342,19 +421,47 @@ export function BrowserViewer({
342
421
  stream: { format: "jpeg", quality: 76, maxWidth: 1_920, maxHeight: 1_200 },
343
422
  ...(webSocketFactory ? { webSocketFactory } : {}),
344
423
  });
345
- const displayedFrame = frameMatchesSelectedTarget(
424
+ const receivedFrame = frameMatchesSelectedTarget(
346
425
  frames.frame,
347
426
  browser.session,
348
427
  browser.selectedTarget,
349
428
  )
350
429
  ? frames.frame
351
430
  : null;
352
- const supportsLiveFrames = browser.session?.capabilities.liveFrames === true;
353
- const displayConnectionState = supportsLiveFrames
354
- ? frames.state === "live" && !displayedFrame
355
- ? "connecting"
356
- : frames.state
357
- : "semantic";
431
+ const displayedFrame = useMemo<BrowserViewportFrame | null>(
432
+ () => (receivedFrame ? { ...receivedFrame, data: receivedFrame.data.slice().buffer } : null),
433
+ [receivedFrame],
434
+ );
435
+ const frameIsLive = frames.state === "live" && displayedFrame !== null;
436
+ useEffect(() => {
437
+ setHasLiveFrame(frameIsLive);
438
+ }, [frameIsLive]);
439
+ const supportsLiveFrames =
440
+ (browser.session ?? selectedRegistrySession)?.capabilities.liveFrames === true;
441
+ const controlUnavailable = isInteractionControlUnavailable(browser.error);
442
+ const connectionError = controlUnavailable ? browser.error : (frames.error ?? browser.error);
443
+ // A managed controller can reject a stale attachment while the same browser
444
+ // remains healthy. Only extension-attached Chrome requires a new browser on
445
+ // connection-generation loss; an unrelated lost Chrome must not poison the
446
+ // selected managed browser's recovery UI.
447
+ const selectedPlacement = (browser.session ?? selectedRegistrySession)?.placement;
448
+ const selectedChromeGenerationLoss =
449
+ selectedPlacement?.kind === "attached_device" &&
450
+ isAttachedChromeGenerationLossError(connectionError);
451
+ const replacementChromeDevice =
452
+ selectedChromeGenerationLoss && selectedPlacement.kind === "attached_device"
453
+ ? attached.devices.find((candidate) => candidate.id === selectedPlacement.deviceId)
454
+ : null;
455
+ const displayConnectionState =
456
+ connectionError && (!frameIsLive || controlUnavailable)
457
+ ? "error"
458
+ : !browser.session
459
+ ? "connecting"
460
+ : supportsLiveFrames
461
+ ? frames.state === "live" && !displayedFrame
462
+ ? "connecting"
463
+ : frames.state
464
+ : "semantic";
358
465
  const selectedProfile = useMemo(
359
466
  () =>
360
467
  profiles.identities.find(
@@ -363,6 +470,15 @@ export function BrowserViewer({
363
470
  ) ?? null,
364
471
  [browser.session?.identityId, profiles.identities, selectedRegistrySession?.identityId],
365
472
  );
473
+ const selectedProfileAuth = useMemo(
474
+ () =>
475
+ selectedProfile
476
+ ? siteAuth.connections.filter(
477
+ (connection) => connection.preferredIdentityId === selectedProfile.id,
478
+ )
479
+ : [],
480
+ [selectedProfile, siteAuth.connections],
481
+ );
366
482
 
367
483
  useEffect(() => {
368
484
  diagnosticRequestRef.current += 1;
@@ -469,8 +585,15 @@ export function BrowserViewer({
469
585
  if (creating) return;
470
586
  const identity =
471
587
  choice.kind === "profile"
472
- ? activeProfiles.find((candidate) => candidate.id === choice.identityId)
588
+ ? profiles.identities.find((candidate) => candidate.id === choice.identityId)
473
589
  : null;
590
+ if (choice.kind === "profile" && (!identity || identity.status !== "active")) {
591
+ onNotify?.({
592
+ kind: "error",
593
+ message: "This browser profile is no longer available. Refresh and choose another.",
594
+ });
595
+ return;
596
+ }
474
597
  const device = choice.kind === "attached" ? choice.device : null;
475
598
  const browserName =
476
599
  device?.profileLabel ?? device?.name ?? (identity ? `${identity.name} browser` : "Browser");
@@ -481,7 +604,7 @@ export function BrowserViewer({
481
604
  if (createLinkedComputer) {
482
605
  try {
483
606
  linkedComputer = await createLinkedComputer(
484
- `${browserName} computer`,
607
+ `${browserName} desktop`,
485
608
  device ? { kind: "attached_device", deviceId: device.id } : undefined,
486
609
  );
487
610
  } catch {
@@ -514,13 +637,20 @@ export function BrowserViewer({
514
637
  placement: linkedComputer.placement,
515
638
  }
516
639
  : {}),
517
- ...(identity ? { identityId: identity.id } : {}),
640
+ ...(identity
641
+ ? {
642
+ identityId: identity.id,
643
+ ...(choice.kind === "profile" && choice.baseRevisionId
644
+ ? { baseRevisionId: choice.baseRevisionId }
645
+ : {}),
646
+ }
647
+ : {}),
518
648
  });
519
- setSelection({ sessionId: response.session.id, pinned: true });
649
+ selectBrowserSession({ sessionId: response.session.id, pinned: true });
520
650
  if (computerViewUnavailable) {
521
651
  onNotify?.({
522
652
  kind: "info",
523
- message: "Browser opened. Computer view is unavailable on this placement.",
653
+ message: "Browser opened. Desktop view is unavailable on this placement.",
524
654
  });
525
655
  }
526
656
  })()
@@ -533,7 +663,8 @@ export function BrowserViewer({
533
663
  creating,
534
664
  notifyError,
535
665
  onNotify,
536
- activeProfiles,
666
+ selectBrowserSession,
667
+ profiles.identities,
537
668
  sessionId,
538
669
  ],
539
670
  );
@@ -647,30 +778,45 @@ export function BrowserViewer({
647
778
  );
648
779
  }
649
780
  if (liveSessions.length === 0) {
650
- if (renderEmpty) return <>{renderEmpty(() => createBrowser(), creating)}</>;
781
+ if (renderEmpty && !unavailableBrowser)
782
+ return <>{renderEmpty(() => createBrowser(), creating)}</>;
651
783
  return (
652
784
  <div
653
785
  className={cn("flex h-full min-h-0 items-center justify-center bg-og-bg p-6", className)}
654
786
  >
655
787
  <div className="max-w-sm text-center">
656
- <span className="mx-auto grid size-10 place-items-center rounded-og-md border border-og-border bg-og-surface-1 text-og-muted">
788
+ <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">
657
789
  <Globe2Icon className="size-4.5" />
658
790
  </span>
659
- <p className="mt-3 text-og-menu font-medium text-og-fg">No browser open</p>
660
- <p className="mt-1 text-og-control leading-5 text-og-muted">
661
- A browser appears here when this agent—or another agent in the workspace—opens one.
662
- </p>
791
+ {unavailableBrowser && !attachedGenerationLoss ? (
792
+ <BrowserUnavailableNotice session={unavailableBrowser} />
793
+ ) : (
794
+ <>
795
+ <p className="mt-3 text-og-menu font-medium text-og-fg">
796
+ {attachedGenerationLoss
797
+ ? "Chrome reconnected—open a fresh browser/desktop."
798
+ : "No browser open"}
799
+ </p>
800
+ <p className="mt-1 text-og-control leading-5 text-og-fg-muted">
801
+ {attachedGenerationLoss
802
+ ? "This Chrome profile is live again, but the previous browser cannot move to the new connection. Use Browser → New browser → Connected Chrome."
803
+ : "A browser appears here when this agent—or another agent in the workspace—opens one."}
804
+ </p>
805
+ </>
806
+ )}
663
807
  <BrowserLaunchMenu
808
+ attachedBridges={onlineAttachedBridges}
664
809
  attachedDevices={attached.devices}
665
810
  identities={profiles.identities}
666
811
  creating={creating}
667
812
  mutatingProfile={savingProfile}
668
813
  onCreate={createBrowser}
669
814
  onRestore={(identity) => updateProfile(identity, { status: "active" })}
815
+ setupUrl={browserExtensionSetupUrl}
670
816
  prominent
671
817
  />
672
818
  {registry.error ? (
673
- <p className="mt-3 text-og-control text-og-status-error">{registry.error.message}</p>
819
+ <p className="mt-3 text-og-control text-og-danger">{registry.error.message}</p>
674
820
  ) : null}
675
821
  </div>
676
822
  </div>
@@ -686,23 +832,31 @@ export function BrowserViewer({
686
832
  relevantSessionIds={new Set(relevant.map((session) => session.id))}
687
833
  selectedSessionId={selection?.sessionId ?? null}
688
834
  identities={profiles.identities}
835
+ attachedBridges={onlineAttachedBridges}
689
836
  attachedDevices={attached.devices}
690
837
  selectedProfile={selectedProfile}
691
838
  baseRevisionOrdinal={baseRevisionOrdinal}
692
839
  profileHistory={profileHistory}
840
+ authConnections={selectedProfileAuth}
693
841
  creating={creating}
694
842
  savingProfile={savingProfile}
695
843
  refreshing={registry.refreshing || attached.refreshing}
696
844
  interventionCounts={interventionCounts}
697
- onSelect={(browserSessionId) => setSelection({ sessionId: browserSessionId, pinned: true })}
845
+ onSelect={(browserSessionId) =>
846
+ selectBrowserSession({ sessionId: browserSessionId, pinned: true })
847
+ }
698
848
  onFollow={() => {
699
849
  const preferred = relevant[0];
700
- if (preferred) setSelection({ sessionId: preferred.id, pinned: false });
850
+ if (preferred) selectBrowserSession({ sessionId: preferred.id, pinned: false });
701
851
  }}
702
852
  onCreate={createBrowser}
703
853
  onSaveProfile={saveProfileVersion}
704
854
  onUpdateProfile={updateProfile}
855
+ onOpenProfileVersion={(identityId, baseRevisionId) =>
856
+ createBrowser({ kind: "profile", identityId, baseRevisionId })
857
+ }
705
858
  onRefresh={() => void Promise.all([registry.refresh(), attached.refresh()])}
859
+ setupUrl={browserExtensionSetupUrl}
706
860
  />
707
861
  <InteractionInterventionBanner
708
862
  interventions={selectedInterventions}
@@ -717,6 +871,7 @@ export function BrowserViewer({
717
871
  />
718
872
  {!selectedRegistrySession ? (
719
873
  <BrowserUnselectedPanel
874
+ unavailableBrowser={unavailableBrowser}
720
875
  peerCount={liveSessions.length}
721
876
  creating={creating}
722
877
  onCreate={() => createBrowser()}
@@ -760,6 +915,22 @@ export function BrowserViewer({
760
915
  .act({ type: "navigate", url })
761
916
  .catch((cause) => notifyError(cause, "Could not navigate."))
762
917
  }
918
+ onHistory={async (direction) => {
919
+ try {
920
+ const action = { type: "history", direction } as const;
921
+ if (receivedFrame) await browser.actFromFrame(action, receivedFrame);
922
+ else {
923
+ if (!browser.observation)
924
+ throw new Error("The browser page is not ready for input.");
925
+ await browser.actFromObservation(action, browser.observation);
926
+ }
927
+ } catch (cause) {
928
+ notifyError(
929
+ cause,
930
+ direction === "back" ? "Could not go back." : "Could not go forward.",
931
+ );
932
+ }
933
+ }}
763
934
  onReload={() => {
764
935
  const url = browser.selectedTarget?.url;
765
936
  if (url)
@@ -768,33 +939,136 @@ export function BrowserViewer({
768
939
  .catch((cause) => notifyError(cause, "Could not reload."));
769
940
  }}
770
941
  />
771
- <BrowserViewport
772
- frame={displayedFrame}
773
- connectionState={displayConnectionState}
774
- supportsLiveFrames={supportsLiveFrames}
775
- connectionError={frames.error}
776
- observation={browser.observation}
777
- mutating={browser.mutating || savingProfile}
778
- activityLabel={savingProfile ? "Saving browser version…" : undefined}
779
- clipboardEnabled={browser.session?.capabilities.clipboard === true}
780
- onAction={async (action, frame) => {
781
- const receipt = frame
782
- ? await browser.actFromFrame(action, frame)
783
- : await browser.act(action);
784
- if (receipt.state !== "completed") {
785
- throw new Error(receipt.error?.message ?? "Browser input did not complete.");
942
+ {browser.inputFailure ? (
943
+ <div
944
+ role="alert"
945
+ aria-label="Browser input status"
946
+ className="flex shrink-0 items-center gap-2 border-t border-og-border bg-og-surface-1 px-3 py-2 text-og-xs"
947
+ >
948
+ <CircleAlertIcon className="size-4 shrink-0 text-og-status-failed" />
949
+ <div className="min-w-0 flex-1">
950
+ <span className="font-medium">
951
+ {browser.inputFailure.state === "failed"
952
+ ? "Browser input failed"
953
+ : "Input result unknown"}
954
+ </span>
955
+ <p className="text-og-fg-subtle">
956
+ {browser.inputFailure.error?.message ?? "Inspect the page before continuing."}
957
+ </p>
958
+ </div>
959
+ <button
960
+ type="button"
961
+ onClick={() => void browser.refresh()}
962
+ className="shrink-0 rounded border border-og-border px-2 py-1 transition hover:bg-og-surface-2"
963
+ >
964
+ Check browser
965
+ </button>
966
+ </div>
967
+ ) : null}
968
+ <div className="relative flex min-h-0 min-w-0 flex-1 overflow-hidden">
969
+ <BrowserViewport
970
+ // A new control scope must discard every buffered gesture and queued action.
971
+ key={JSON.stringify([
972
+ selection?.sessionId,
973
+ browser.selectedTarget?.id,
974
+ browser.selectedTarget?.targetGeneration,
975
+ browser.selectedTarget?.documentGeneration,
976
+ frames.attachment?.controllerGeneration,
977
+ ])}
978
+ focusHandoffRef={viewportFocusHandoffRef}
979
+ focusScope={JSON.stringify([
980
+ selection?.sessionId,
981
+ browser.selectedTarget?.id,
982
+ browser.selectedTarget?.targetGeneration,
983
+ ])}
984
+ frame={displayedFrame}
985
+ connectionState={displayConnectionState}
986
+ supportsLiveFrames={supportsLiveFrames}
987
+ connectionError={connectionError}
988
+ controlUnavailable={controlUnavailable}
989
+ observation={browser.observation}
990
+ mutating={browser.mutating || savingProfile}
991
+ activityLabel={savingProfile ? "Saving browser version…" : undefined}
992
+ clipboardEnabled={browser.session?.capabilities.clipboard === true}
993
+ inputBatchAttachment={frames.attachment}
994
+ onAction={async (action, frame) => {
995
+ const focusedInput =
996
+ frame &&
997
+ action.type === "pointer" &&
998
+ action.action === "click" &&
999
+ (action.button === undefined || action.button === "left") &&
1000
+ frames.attachment?.focusedInputObservations === true &&
1001
+ frames.attachment.browserSessionId === frame.browserSessionId &&
1002
+ frames.attachment.controllerGeneration === frame.controllerGeneration &&
1003
+ frames.attachment.targetId === frame.targetId &&
1004
+ Date.parse(frames.attachment.expiresAt) > Date.now();
1005
+ const receipt = frame
1006
+ ? await browser.actFromFrame(
1007
+ action,
1008
+ frame,
1009
+ undefined,
1010
+ focusedInput ? "input" : "none",
1011
+ )
1012
+ : await browser.act(action);
1013
+ if (receipt.state !== "completed") {
1014
+ throw new Error(receipt.error?.message ?? "Browser input did not complete.");
1015
+ }
1016
+ return receipt;
1017
+ }}
1018
+ onReadClipboard={browser.readClipboard}
1019
+ onObserveForInput={browser.observeForInput}
1020
+ onSelectFromObservation={browser.actFromObservation}
1021
+ onReconnect={
1022
+ selectedChromeGenerationLoss
1023
+ ? () => {
1024
+ if (replacementChromeDevice) {
1025
+ createBrowser({ kind: "attached", device: replacementChromeDevice });
1026
+ }
1027
+ }
1028
+ : () => {
1029
+ void browser.refresh();
1030
+ frames.reconnect();
1031
+ }
786
1032
  }
787
- return receipt;
788
- }}
789
- onReadClipboard={browser.readClipboard}
790
- onReconnect={frames.reconnect}
791
- onError={(cause) => notifyError(cause, "Browser input failed.")}
792
- />
1033
+ reconnectLabel={
1034
+ selectedChromeGenerationLoss && replacementChromeDevice
1035
+ ? "Open a fresh Connected Chrome"
1036
+ : undefined
1037
+ }
1038
+ reconnectMessage={
1039
+ selectedChromeGenerationLoss
1040
+ ? "Chrome reconnected—open a fresh browser/desktop."
1041
+ : undefined
1042
+ }
1043
+ onError={(cause) => notifyError(cause, "Browser input failed.")}
1044
+ />
1045
+ {diagnosticsView ? (
1046
+ <BrowserDiagnosticsDrawer
1047
+ session={browser.session}
1048
+ profile={selectedProfile}
1049
+ target={browser.selectedTarget}
1050
+ observation={browser.observation}
1051
+ baseRevisionOrdinal={baseRevisionOrdinal}
1052
+ state={diagnosticsView}
1053
+ downloads={downloads.downloads}
1054
+ downloadsEnabled={browser.session?.capabilities.downloads === true}
1055
+ downloadsLoading={downloads.loading}
1056
+ downloadsRefreshing={downloads.refreshing}
1057
+ downloadsError={downloads.error}
1058
+ savingDownloadIds={downloads.savingDownloadIds}
1059
+ onSaveDownload={saveDownload}
1060
+ onRefreshDownloads={() => void downloads.refresh()}
1061
+ onRefresh={refreshDiagnostics}
1062
+ onClose={closeDiagnostics}
1063
+ />
1064
+ ) : null}
1065
+ </div>
793
1066
  <BrowserStatusBar
794
1067
  session={browser.session}
795
1068
  profile={selectedProfile}
796
1069
  target={browser.selectedTarget}
797
1070
  observation={browser.observation}
1071
+ inputFailure={browser.inputFailure}
798
1072
  connectionState={displayConnectionState}
799
1073
  refreshing={registry.refreshing}
800
1074
  diagnosticsOpen={diagnosticsView !== null}
@@ -810,26 +1084,6 @@ export function BrowserViewer({
810
1084
  />
811
1085
  </>
812
1086
  )}
813
- {diagnosticsView ? (
814
- <BrowserDiagnosticsDrawer
815
- session={browser.session}
816
- profile={selectedProfile}
817
- target={browser.selectedTarget}
818
- observation={browser.observation}
819
- baseRevisionOrdinal={baseRevisionOrdinal}
820
- state={diagnosticsView}
821
- downloads={downloads.downloads}
822
- downloadsEnabled={browser.session?.capabilities.downloads === true}
823
- downloadsLoading={downloads.loading}
824
- downloadsRefreshing={downloads.refreshing}
825
- downloadsError={downloads.error}
826
- savingDownloadIds={downloads.savingDownloadIds}
827
- onSaveDownload={saveDownload}
828
- onRefreshDownloads={() => void downloads.refresh()}
829
- onRefresh={refreshDiagnostics}
830
- onClose={closeDiagnostics}
831
- />
832
- ) : null}
833
1087
  </div>
834
1088
  );
835
1089
  }
@@ -857,20 +1111,20 @@ function BrowserLifecyclePanel(props: {
857
1111
  return (
858
1112
  <div className="grid min-h-0 flex-1 place-items-center bg-og-bg p-6 text-center">
859
1113
  <div className="max-w-sm">
860
- <span className="mx-auto grid size-10 place-items-center rounded-og-md border border-og-border bg-og-surface-1 text-og-muted">
1114
+ <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">
861
1115
  {failed ? (
862
- <CircleAlertIcon className="size-4.5 text-og-status-error" />
1116
+ <CircleAlertIcon className="size-4.5 text-og-danger" />
863
1117
  ) : (
864
1118
  <LoaderCircleIcon className="size-4.5 animate-spin" />
865
1119
  )}
866
1120
  </span>
867
1121
  <p className="mt-3 text-og-menu font-medium text-og-fg">{label}</p>
868
- <p className="mt-1 text-og-control leading-5 text-og-muted">{detail}</p>
1122
+ <p className="mt-1 text-og-control leading-5 text-og-fg-muted">{detail}</p>
869
1123
  {failed || (props.session.lifecycle === "suspended" && !props.attempt?.running) ? (
870
1124
  <button
871
1125
  type="button"
872
1126
  onClick={props.onResume}
873
- 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"
1127
+ 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"
874
1128
  >
875
1129
  Open browser
876
1130
  </button>
@@ -884,11 +1138,13 @@ function BrowserToolbar(props: {
884
1138
  sessions: BrowserSession[];
885
1139
  relevantSessionIds: Set<string>;
886
1140
  selectedSessionId: string | null;
1141
+ attachedBridges: AttachedBrowserBridge[];
887
1142
  attachedDevices: AttachedBrowserDevice[];
888
1143
  identities: BrowserIdentity[];
889
1144
  selectedProfile: BrowserIdentity | null;
890
1145
  baseRevisionOrdinal: number | null;
891
1146
  profileHistory: { identityId: string; loading: boolean; revisions: BrowserRevision[] } | null;
1147
+ authConnections: SiteAuthConnection[];
892
1148
  creating: boolean;
893
1149
  savingProfile: boolean;
894
1150
  refreshing: boolean;
@@ -901,9 +1157,11 @@ function BrowserToolbar(props: {
901
1157
  identity: BrowserIdentity,
902
1158
  update: { name?: string; status?: "active" | "archived"; defaultRevisionId?: string },
903
1159
  ) => Promise<boolean>;
1160
+ onOpenProfileVersion: (identityId: string, baseRevisionId: string) => void;
904
1161
  onRefresh: () => void;
1162
+ setupUrl?: string | undefined;
905
1163
  }) {
906
- const detailsRef = useRef<HTMLDetailsElement | null>(null);
1164
+ const detailsRef = useViewerMenuDismiss();
907
1165
  const selected = props.sessions.find((session) => session.id === props.selectedSessionId);
908
1166
  const current = props.sessions.filter((session) => props.relevantSessionIds.has(session.id));
909
1167
  const others = props.sessions.filter((session) => !props.relevantSessionIds.has(session.id));
@@ -912,34 +1170,36 @@ function BrowserToolbar(props: {
912
1170
  detailsRef.current?.removeAttribute("open");
913
1171
  };
914
1172
  return (
915
- <div className="flex h-10 shrink-0 items-center gap-1.5 border-b border-og-border bg-og-surface-0 px-2">
916
- <details ref={detailsRef} className="relative min-w-0">
1173
+ <div className="flex h-10 shrink-0 items-center gap-1.5 border-b border-og-border bg-og-surface-1 px-2">
1174
+ <details ref={detailsRef} className="min-w-0">
917
1175
  <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">
918
- <Globe2Icon className="size-3.5 shrink-0 text-og-muted" />
1176
+ <Globe2Icon className="size-3.5 shrink-0 text-og-fg-muted" />
919
1177
  <span className="truncate font-medium">{selected?.name ?? "Browser"}</span>
920
1178
  {(props.interventionCounts.get(selected?.id ?? "") ?? 0) > 0 ? (
921
1179
  <span className="size-1.5 shrink-0 rounded-full bg-og-status-waiting" />
922
1180
  ) : null}
923
- <ChevronDownIcon className="size-3 shrink-0 text-og-subtle" />
1181
+ <ChevronDownIcon className="size-3 shrink-0 text-og-fg-subtle" />
924
1182
  </summary>
925
- <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">
926
- <BrowserSessionGroup
927
- label="Current agent"
928
- sessions={current}
929
- identities={props.identities}
930
- selectedId={props.selectedSessionId}
931
- interventionCounts={props.interventionCounts}
932
- onSelect={choose}
933
- />
934
- <BrowserSessionGroup
935
- label="Workspace browsers"
936
- sessions={others}
937
- identities={props.identities}
938
- selectedId={props.selectedSessionId}
939
- interventionCounts={props.interventionCounts}
940
- onSelect={choose}
941
- />
942
- <div className="mt-1 flex gap-1 border-t border-og-border pt-1">
1183
+ <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">
1184
+ <div className="min-h-0 max-h-96 overflow-y-auto overscroll-contain">
1185
+ <BrowserSessionGroup
1186
+ label="Current agent"
1187
+ sessions={current}
1188
+ identities={props.identities}
1189
+ selectedId={props.selectedSessionId}
1190
+ interventionCounts={props.interventionCounts}
1191
+ onSelect={choose}
1192
+ />
1193
+ <BrowserSessionGroup
1194
+ label="Workspace browsers"
1195
+ sessions={others}
1196
+ identities={props.identities}
1197
+ selectedId={props.selectedSessionId}
1198
+ interventionCounts={props.interventionCounts}
1199
+ onSelect={choose}
1200
+ />
1201
+ </div>
1202
+ <div className="mt-1 flex shrink-0 gap-1 border-t border-og-border pt-1">
943
1203
  {current.length > 0 ? (
944
1204
  <MenuButton onClick={props.onFollow}>Follow agent</MenuButton>
945
1205
  ) : null}
@@ -949,7 +1209,7 @@ function BrowserToolbar(props: {
949
1209
  </div>
950
1210
  </div>
951
1211
  </details>
952
- <span className="min-w-0 flex-1 truncate text-og-xs text-og-subtle">
1212
+ <span className="min-w-0 flex-1 truncate text-og-xs text-og-fg-subtle">
953
1213
  {placementLabel(selected)}
954
1214
  </span>
955
1215
  <BrowserProfileMenu
@@ -957,31 +1217,36 @@ function BrowserToolbar(props: {
957
1217
  identity={props.selectedProfile}
958
1218
  baseRevisionOrdinal={props.baseRevisionOrdinal}
959
1219
  history={props.profileHistory}
1220
+ authConnections={props.authConnections}
960
1221
  saving={props.savingProfile}
961
1222
  onSave={props.onSaveProfile}
962
1223
  onUpdate={props.onUpdateProfile}
1224
+ onOpenVersion={props.onOpenProfileVersion}
963
1225
  />
964
1226
  <button
965
1227
  type="button"
966
1228
  onClick={props.onRefresh}
967
- className="grid size-7 place-items-center rounded-og-sm text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg"
1229
+ 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"
968
1230
  aria-label="Refresh browsers"
969
1231
  >
970
1232
  <RefreshCwIcon className={cn("size-3.5", props.refreshing && "animate-spin")} />
971
1233
  </button>
972
1234
  <BrowserLaunchMenu
1235
+ attachedBridges={props.attachedBridges}
973
1236
  attachedDevices={props.attachedDevices}
974
1237
  identities={props.identities}
975
1238
  creating={props.creating}
976
1239
  mutatingProfile={props.savingProfile}
977
1240
  onCreate={props.onCreate}
978
1241
  onRestore={(identity) => props.onUpdateProfile(identity, { status: "active" })}
1242
+ setupUrl={props.setupUrl}
979
1243
  />
980
1244
  </div>
981
1245
  );
982
1246
  }
983
1247
 
984
1248
  function BrowserUnselectedPanel(props: {
1249
+ unavailableBrowser: BrowserSession | undefined;
985
1250
  peerCount: number;
986
1251
  creating: boolean;
987
1252
  onCreate: () => void;
@@ -989,11 +1254,15 @@ function BrowserUnselectedPanel(props: {
989
1254
  return (
990
1255
  <div className="grid min-h-0 flex-1 place-items-center bg-og-bg p-6 text-center">
991
1256
  <div className="max-w-sm">
992
- <span className="mx-auto grid size-10 place-items-center rounded-og-md border border-og-border bg-og-surface-1 text-og-muted">
1257
+ <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">
993
1258
  <Globe2Icon className="size-4.5" />
994
1259
  </span>
995
- <p className="mt-3 text-og-menu font-medium text-og-fg">No browser for this agent</p>
996
- <p className="mt-1 text-og-control leading-5 text-og-muted">
1260
+ {props.unavailableBrowser ? (
1261
+ <BrowserUnavailableNotice session={props.unavailableBrowser} />
1262
+ ) : (
1263
+ <p className="mt-3 text-og-menu font-medium text-og-fg">No browser for this agent</p>
1264
+ )}
1265
+ <p className="mt-1 text-og-control leading-5 text-og-fg-muted">
997
1266
  {props.peerCount === 1
998
1267
  ? "One workspace browser is available from the browser menu."
999
1268
  : `${props.peerCount} workspace browsers are available from the browser menu.`}
@@ -1002,7 +1271,7 @@ function BrowserUnselectedPanel(props: {
1002
1271
  type="button"
1003
1272
  disabled={props.creating}
1004
1273
  onClick={props.onCreate}
1005
- 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"
1274
+ 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"
1006
1275
  >
1007
1276
  <PlusIcon className="size-3.5" />
1008
1277
  {props.creating ? "Opening…" : "New browser"}
@@ -1012,6 +1281,25 @@ function BrowserUnselectedPanel(props: {
1012
1281
  );
1013
1282
  }
1014
1283
 
1284
+ function BrowserUnavailableNotice({ session }: { session: BrowserSession }) {
1285
+ const reason =
1286
+ session.failureCode === "provider_deadline_rotation"
1287
+ ? "The computer running it reached its time limit."
1288
+ : session.failureCode === "source_placement_changed"
1289
+ ? "This chat moved to another computer."
1290
+ : session.lifecycle === "failed"
1291
+ ? "It stopped unexpectedly."
1292
+ : "The connection to it was lost.";
1293
+ return (
1294
+ <div role="status">
1295
+ <p className="mt-3 text-og-menu font-medium text-og-fg">Browser unavailable</p>
1296
+ <p className="mt-1 break-words text-og-control leading-5 text-og-fg-muted">
1297
+ {session.name}. {reason} Open a new browser to continue.
1298
+ </p>
1299
+ </div>
1300
+ );
1301
+ }
1302
+
1015
1303
  function BrowserSessionGroup(props: {
1016
1304
  label: string;
1017
1305
  sessions: BrowserSession[];
@@ -1023,7 +1311,7 @@ function BrowserSessionGroup(props: {
1023
1311
  if (props.sessions.length === 0) return null;
1024
1312
  return (
1025
1313
  <div className="py-1">
1026
- <p className="px-2 pb-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-subtle">
1314
+ <p className="px-2 pb-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-fg-subtle">
1027
1315
  {props.label}
1028
1316
  </p>
1029
1317
  {props.sessions.map((session) => {
@@ -1033,6 +1321,8 @@ function BrowserSessionGroup(props: {
1033
1321
  <button
1034
1322
  key={session.id}
1035
1323
  type="button"
1324
+ aria-label={`${session.name}${identity ? ` · ${identity.name}` : ""} · ${placementLabel(session)}`}
1325
+ aria-pressed={session.id === props.selectedId}
1036
1326
  onClick={() => props.onSelect(session.id)}
1037
1327
  className={cn(
1038
1328
  "flex w-full items-center gap-2 rounded-og-sm px-2 py-1.5 text-left transition hover:bg-og-surface-2",
@@ -1047,7 +1337,7 @@ function BrowserSessionGroup(props: {
1047
1337
  />
1048
1338
  <span className="min-w-0 flex-1">
1049
1339
  <span className="block truncate text-og-control text-og-fg">{session.name}</span>
1050
- <span className="block truncate text-og-xs text-og-subtle">
1340
+ <span className="block truncate text-og-xs text-og-fg-subtle">
1051
1341
  {identity ? `${identity.name} · ` : ""}
1052
1342
  {placementLabel(session)}
1053
1343
  </span>
@@ -1065,15 +1355,17 @@ function BrowserSessionGroup(props: {
1065
1355
  }
1066
1356
 
1067
1357
  function BrowserLaunchMenu(props: {
1358
+ attachedBridges: AttachedBrowserBridge[];
1068
1359
  attachedDevices: AttachedBrowserDevice[];
1069
1360
  identities: BrowserIdentity[];
1070
1361
  creating: boolean;
1071
1362
  mutatingProfile: boolean;
1072
1363
  onCreate: (choice?: BrowserLaunchChoice) => void;
1073
1364
  onRestore: (identity: BrowserIdentity) => Promise<boolean>;
1365
+ setupUrl?: string | undefined;
1074
1366
  prominent?: boolean;
1075
1367
  }) {
1076
- const detailsRef = useRef<HTMLDetailsElement | null>(null);
1368
+ const detailsRef = useViewerMenuDismiss();
1077
1369
  const choose = (choice?: BrowserLaunchChoice) => {
1078
1370
  detailsRef.current?.removeAttribute("open");
1079
1371
  props.onCreate(choice);
@@ -1085,13 +1377,13 @@ function BrowserLaunchMenu(props: {
1085
1377
  const activeIdentities = props.identities.filter((identity) => identity.status === "active");
1086
1378
  const archivedIdentities = props.identities.filter((identity) => identity.status === "archived");
1087
1379
  return (
1088
- <details ref={detailsRef} className={cn("relative", props.prominent && "mt-4 inline-block")}>
1380
+ <details ref={detailsRef} className={cn(props.prominent && "mt-4 inline-block")}>
1089
1381
  <summary
1090
1382
  onClick={(event) => {
1091
1383
  if (busy) event.preventDefault();
1092
1384
  }}
1093
1385
  className={cn(
1094
- "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",
1386
+ "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",
1095
1387
  props.prominent
1096
1388
  ? "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"
1097
1389
  : "grid size-7 place-items-center rounded-og-sm",
@@ -1109,13 +1401,13 @@ function BrowserLaunchMenu(props: {
1109
1401
  </summary>
1110
1402
  <div
1111
1403
  className={cn(
1112
- "absolute z-40 w-72 overflow-hidden rounded-og-md border border-og-border bg-og-surface-1 p-1 text-left shadow-xl",
1113
- props.prominent ? "left-1/2 top-10 -translate-x-1/2" : "right-0 top-8",
1404
+ "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",
1405
+ props.prominent ? "left-1/2 top-2 -translate-x-1/2" : "right-2 top-10",
1114
1406
  )}
1115
1407
  >
1116
- {props.attachedDevices.length > 0 ? (
1408
+ {props.attachedDevices.length > 0 || props.attachedBridges.length > 0 ? (
1117
1409
  <div className="pb-1">
1118
- <p className="px-2 pb-1 pt-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-subtle">
1410
+ <p className="px-2 pb-1 pt-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-fg-subtle">
1119
1411
  Connected Chrome
1120
1412
  </p>
1121
1413
  {props.attachedDevices.map((device) => (
@@ -1125,12 +1417,12 @@ function BrowserLaunchMenu(props: {
1125
1417
  onClick={() => choose({ kind: "attached", device })}
1126
1418
  className="flex w-full items-center gap-2 rounded-og-sm px-2 py-2 text-left transition hover:bg-og-surface-2"
1127
1419
  >
1128
- <MonitorIcon className="size-3.5 shrink-0 text-og-muted" />
1420
+ <MonitorIcon className="size-3.5 shrink-0 text-og-fg-muted" />
1129
1421
  <span className="min-w-0 flex-1">
1130
1422
  <span className="block truncate text-og-control text-og-fg">
1131
1423
  {device.profileLabel ?? device.name}
1132
1424
  </span>
1133
- <span className="block truncate text-og-xs text-og-subtle">
1425
+ <span className="block truncate text-og-xs text-og-fg-subtle">
1134
1426
  {device.browserName} · {device.tabCount} open{" "}
1135
1427
  {device.tabCount === 1 ? "tab" : "tabs"}
1136
1428
  </span>
@@ -1138,12 +1430,48 @@ function BrowserLaunchMenu(props: {
1138
1430
  <span className="size-1.5 shrink-0 rounded-full bg-og-status-running" />
1139
1431
  </button>
1140
1432
  ))}
1433
+ {props.attachedBridges.length > 0 ? (
1434
+ props.setupUrl ? (
1435
+ <a
1436
+ href={props.setupUrl}
1437
+ target="_blank"
1438
+ rel="noreferrer"
1439
+ onClick={() => detailsRef.current?.removeAttribute("open")}
1440
+ className="flex w-full items-center gap-2 rounded-og-sm px-2 py-2 text-left transition hover:bg-og-surface-2"
1441
+ >
1442
+ <PlusIcon className="size-3.5 shrink-0 text-og-fg-muted" />
1443
+ <span className="min-w-0 flex-1">
1444
+ <span className="block truncate text-og-control text-og-fg">
1445
+ {props.attachedDevices.length > 0
1446
+ ? "Connect another Chrome profile"
1447
+ : "Connect this Chrome profile"}
1448
+ </span>
1449
+ <span className="block truncate text-og-xs text-og-fg-subtle">
1450
+ Machine connected · Chrome extension missing
1451
+ </span>
1452
+ </span>
1453
+ </a>
1454
+ ) : (
1455
+ <div className="flex w-full items-center gap-2 px-2 py-2 text-left">
1456
+ <MonitorIcon className="size-3.5 shrink-0 text-og-fg-muted" />
1457
+ <span className="min-w-0 flex-1">
1458
+ <span className="block truncate text-og-control text-og-fg">
1459
+ Chrome connection ready
1460
+ </span>
1461
+ <span className="block text-og-xs leading-4 text-og-fg-subtle">
1462
+ Install the Opengeni Browser extension in the profile you want to use.
1463
+ </span>
1464
+ </span>
1465
+ </div>
1466
+ )
1467
+ ) : null}
1141
1468
  </div>
1142
1469
  ) : null}
1143
1470
  <p
1144
1471
  className={cn(
1145
- "px-2 pb-1 pt-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-subtle",
1146
- props.attachedDevices.length > 0 && "border-t border-og-border",
1472
+ "px-2 pb-1 pt-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-fg-subtle",
1473
+ (props.attachedDevices.length > 0 || props.attachedBridges.length > 0) &&
1474
+ "border-t border-og-border",
1147
1475
  )}
1148
1476
  >
1149
1477
  Open isolated browser
@@ -1153,10 +1481,12 @@ function BrowserLaunchMenu(props: {
1153
1481
  onClick={() => choose({ kind: "clean" })}
1154
1482
  className="flex w-full items-center gap-2 rounded-og-sm px-2 py-2 text-left transition hover:bg-og-surface-2"
1155
1483
  >
1156
- <Globe2Icon className="size-3.5 text-og-muted" />
1484
+ <Globe2Icon className="size-3.5 text-og-fg-muted" />
1157
1485
  <span>
1158
1486
  <span className="block text-og-control text-og-fg">Fresh browser</span>
1159
- <span className="block text-og-xs text-og-subtle">Visual browser · no saved state</span>
1487
+ <span className="block text-og-xs text-og-fg-subtle">
1488
+ Visual browser · no saved state
1489
+ </span>
1160
1490
  </span>
1161
1491
  </button>
1162
1492
  {activeIdentities.length > 0 ? (
@@ -1168,10 +1498,10 @@ function BrowserLaunchMenu(props: {
1168
1498
  onClick={() => choose({ kind: "profile", identityId: identity.id })}
1169
1499
  className="flex w-full items-center gap-2 rounded-og-sm px-2 py-2 text-left transition hover:bg-og-surface-2"
1170
1500
  >
1171
- <UserRoundIcon className="size-3.5 text-og-muted" />
1501
+ <UserRoundIcon className="size-3.5 text-og-fg-muted" />
1172
1502
  <span className="min-w-0">
1173
1503
  <span className="block truncate text-og-control text-og-fg">{identity.name}</span>
1174
- <span className="block text-og-xs text-og-subtle">
1504
+ <span className="block text-og-xs text-og-fg-subtle">
1175
1505
  {identity.defaultRevisionId
1176
1506
  ? `${identity.revisionCount} saved version${identity.revisionCount === 1 ? "" : "s"}`
1177
1507
  : "Ready for first sign-in"}
@@ -1183,7 +1513,7 @@ function BrowserLaunchMenu(props: {
1183
1513
  ) : null}
1184
1514
  {archivedIdentities.length > 0 ? (
1185
1515
  <details className="mt-1 border-t border-og-border pt-1">
1186
- <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">
1516
+ <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">
1187
1517
  <ArchiveIcon className="size-3" /> Hidden profiles · {archivedIdentities.length}
1188
1518
  <ChevronDownIcon className="ml-auto size-3" />
1189
1519
  </summary>
@@ -1195,7 +1525,7 @@ function BrowserLaunchMenu(props: {
1195
1525
  onClick={() => void restore(identity)}
1196
1526
  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"
1197
1527
  >
1198
- <RotateCcwIcon className="size-3.5 text-og-muted" />
1528
+ <RotateCcwIcon className="size-3.5 text-og-fg-muted" />
1199
1529
  <span className="min-w-0 flex-1 truncate text-og-control text-og-fg">
1200
1530
  Restore {identity.name}
1201
1531
  </span>
@@ -1213,14 +1543,16 @@ function BrowserProfileMenu(props: {
1213
1543
  identity: BrowserIdentity | null;
1214
1544
  baseRevisionOrdinal: number | null;
1215
1545
  history: { identityId: string; loading: boolean; revisions: BrowserRevision[] } | null;
1546
+ authConnections: SiteAuthConnection[];
1216
1547
  saving: boolean;
1217
1548
  onSave: (newProfileName?: string) => Promise<boolean>;
1218
1549
  onUpdate: (
1219
1550
  identity: BrowserIdentity,
1220
1551
  update: { name?: string; status?: "active" | "archived"; defaultRevisionId?: string },
1221
1552
  ) => Promise<boolean>;
1553
+ onOpenVersion: (identityId: string, baseRevisionId: string) => void;
1222
1554
  }) {
1223
- const detailsRef = useRef<HTMLDetailsElement | null>(null);
1555
+ const detailsRef = useViewerMenuDismiss();
1224
1556
  const nameInputId = useId();
1225
1557
  const [name, setName] = useState("");
1226
1558
  const attached = props.session?.placement.kind === "attached_device";
@@ -1237,6 +1569,10 @@ function BrowserProfileMenu(props: {
1237
1569
  detailsRef.current?.removeAttribute("open");
1238
1570
  }
1239
1571
  };
1572
+ const openVersion = (identityId: string, baseRevisionId: string) => {
1573
+ detailsRef.current?.removeAttribute("open");
1574
+ props.onOpenVersion(identityId, baseRevisionId);
1575
+ };
1240
1576
  const versionLabel = attached
1241
1577
  ? "live"
1242
1578
  : props.identity
@@ -1247,8 +1583,8 @@ function BrowserProfileMenu(props: {
1247
1583
  : "unsaved"
1248
1584
  : "temporary";
1249
1585
  return (
1250
- <details ref={detailsRef} className="relative min-w-0">
1251
- <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">
1586
+ <details ref={detailsRef} className="min-w-0">
1587
+ <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">
1252
1588
  {props.saving ? (
1253
1589
  <LoaderCircleIcon className="size-3 animate-spin" />
1254
1590
  ) : attached ? (
@@ -1260,13 +1596,13 @@ function BrowserProfileMenu(props: {
1260
1596
  {attached ? props.session?.name : (props.identity?.name ?? "Temporary")}
1261
1597
  </span>
1262
1598
  {attached || props.identity ? (
1263
- <span className="shrink-0 text-og-subtle">· {versionLabel}</span>
1599
+ <span className="shrink-0 text-og-fg-subtle">· {versionLabel}</span>
1264
1600
  ) : null}
1265
- <ChevronDownIcon className="size-3 shrink-0 text-og-subtle" />
1601
+ <ChevronDownIcon className="size-3 shrink-0 text-og-fg-subtle" />
1266
1602
  </summary>
1267
- <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">
1603
+ <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">
1268
1604
  <div className="flex items-start gap-2">
1269
- <span className="grid size-7 shrink-0 place-items-center rounded-og-sm bg-og-surface-2 text-og-muted">
1605
+ <span className="grid size-7 shrink-0 place-items-center rounded-og-sm bg-og-surface-2 text-og-fg-muted">
1270
1606
  {attached ? (
1271
1607
  <MonitorIcon className="size-3.5" />
1272
1608
  ) : (
@@ -1277,7 +1613,7 @@ function BrowserProfileMenu(props: {
1277
1613
  <p className="truncate text-og-control font-medium text-og-fg">
1278
1614
  {attached ? props.session?.name : (props.identity?.name ?? "Temporary browser")}
1279
1615
  </p>
1280
- <p className="mt-0.5 text-og-xs leading-4 text-og-subtle">
1616
+ <p className="mt-0.5 text-og-xs leading-4 text-og-fg-subtle">
1281
1617
  {attached
1282
1618
  ? "This session drives the existing Chrome profile and its current login state."
1283
1619
  : props.identity?.status === "archived"
@@ -1292,53 +1628,101 @@ function BrowserProfileMenu(props: {
1292
1628
  </div>
1293
1629
  {props.identity && revisions.length > 0 ? (
1294
1630
  <div className="mt-3 border-t border-og-border pt-2">
1295
- <p className="px-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-subtle">
1631
+ <p className="px-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-fg-subtle">
1296
1632
  Saved versions
1297
1633
  </p>
1298
1634
  <div className="mt-1 max-h-40 overflow-y-auto">
1299
1635
  {revisions.map((revision) => {
1300
1636
  const isDefault = revision.id === props.identity?.defaultRevisionId;
1301
1637
  const isCurrent = revision.ordinal === props.baseRevisionOrdinal;
1638
+ const materialization = revisionMaterializationSummary(revision);
1302
1639
  return (
1303
- <button
1640
+ <div
1304
1641
  key={revision.id}
1305
- type="button"
1306
- disabled={isDefault || props.saving || props.identity?.status === "archived"}
1307
- onClick={() =>
1308
- void props.onUpdate(props.identity!, { defaultRevisionId: revision.id })
1309
- }
1310
- className="flex w-full items-center gap-2 rounded-og-sm px-2 py-1.5 text-left transition hover:bg-og-surface-2 disabled:cursor-default disabled:opacity-70"
1642
+ className="flex w-full items-center gap-1 rounded-og-sm transition hover:bg-og-surface-2"
1311
1643
  >
1312
- <span className="grid size-4 place-items-center text-og-muted">
1313
- {isDefault ? <CheckIcon className="size-3.5 text-og-status-running" /> : null}
1314
- </span>
1315
- <span className="min-w-0 flex-1 text-og-control text-og-fg">
1316
- Version {revision.ordinal}
1317
- </span>
1318
- <span className="text-[10px] text-og-subtle">
1319
- {isCurrent && isDefault
1320
- ? "current · default"
1321
- : isDefault
1322
- ? "default"
1323
- : isCurrent
1324
- ? "current · Use by default"
1325
- : "Use by default"}
1326
- </span>
1327
- </button>
1644
+ <button
1645
+ type="button"
1646
+ disabled={props.saving || props.identity?.status === "archived"}
1647
+ aria-label={`Open ${props.identity!.name} version ${revision.ordinal}`}
1648
+ onClick={() => openVersion(props.identity!.id, revision.id)}
1649
+ className="flex min-w-0 flex-1 items-center gap-2 px-2 py-1.5 text-left disabled:opacity-50"
1650
+ >
1651
+ <span className="grid size-4 place-items-center text-og-fg-muted">
1652
+ {isCurrent ? <CheckIcon className="size-3.5 text-og-accent" /> : null}
1653
+ </span>
1654
+ <span className="min-w-0 flex-1">
1655
+ <span className="block text-og-control text-og-fg">
1656
+ Version {revision.ordinal}
1657
+ </span>
1658
+ <span className="block truncate text-[10px] text-og-fg-subtle">
1659
+ {materialization.label}
1660
+ </span>
1661
+ </span>
1662
+ {isCurrent ? (
1663
+ <span className="text-[10px] text-og-fg-subtle">Current</span>
1664
+ ) : null}
1665
+ </button>
1666
+ <button
1667
+ type="button"
1668
+ disabled={isDefault || props.saving || props.identity?.status === "archived"}
1669
+ aria-label={
1670
+ isDefault
1671
+ ? `${props.identity!.name} version ${revision.ordinal} is the default`
1672
+ : `Use ${props.identity!.name} version ${revision.ordinal} by default`
1673
+ }
1674
+ title={isDefault ? "Default version" : "Use by default"}
1675
+ onClick={() =>
1676
+ void props.onUpdate(props.identity!, { defaultRevisionId: revision.id })
1677
+ }
1678
+ 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"
1679
+ >
1680
+ {isDefault ? "Default" : "Make default"}
1681
+ </button>
1682
+ </div>
1328
1683
  );
1329
1684
  })}
1330
1685
  </div>
1331
- <p className="mt-1 px-2 text-[10px] leading-4 text-og-subtle">
1332
- Changing the default affects only browsers opened later.
1686
+ <p className="mt-1 px-2 text-[10px] leading-4 text-og-fg-subtle">
1687
+ Changing the default affects only browsers opened later. Saved browser data can be
1688
+ copied; a website may still expire or re-verify its own session.
1333
1689
  </p>
1334
1690
  </div>
1335
1691
  ) : props.identity &&
1336
1692
  props.history?.identityId === props.identity.id &&
1337
1693
  props.history.loading ? (
1338
- <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">
1694
+ <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">
1339
1695
  <LoaderCircleIcon className="size-3 animate-spin" /> Loading saved versions…
1340
1696
  </p>
1341
1697
  ) : null}
1698
+ {props.identity && props.authConnections.length > 0 ? (
1699
+ <div className="mt-3 border-t border-og-border pt-2">
1700
+ <p className="px-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-fg-subtle">
1701
+ Login health
1702
+ </p>
1703
+ <div className="mt-1 space-y-1">
1704
+ {props.authConnections.map((connection) => {
1705
+ const health = siteAuthHealth(connection);
1706
+ return (
1707
+ <div
1708
+ key={connection.id}
1709
+ className="flex items-start gap-2 rounded-og-sm px-2 py-1.5"
1710
+ >
1711
+ <span className={cn("mt-1 size-1.5 shrink-0 rounded-full", health.dotClass)} />
1712
+ <span className="min-w-0 flex-1">
1713
+ <span className="block truncate text-og-control text-og-fg">
1714
+ {connection.name}
1715
+ </span>
1716
+ <span className="block truncate text-[10px] text-og-fg-subtle">
1717
+ {connection.accountLabel} · {health.label}
1718
+ </span>
1719
+ </span>
1720
+ </div>
1721
+ );
1722
+ })}
1723
+ </div>
1724
+ </div>
1725
+ ) : null}
1342
1726
  {canSave ? (
1343
1727
  props.identity ? (
1344
1728
  <button
@@ -1362,7 +1746,7 @@ function BrowserProfileMenu(props: {
1362
1746
  if (name.trim()) void save(name);
1363
1747
  }}
1364
1748
  >
1365
- <label className="text-og-xs font-medium text-og-muted" htmlFor={nameInputId}>
1749
+ <label className="text-og-xs font-medium text-og-fg-muted" htmlFor={nameInputId}>
1366
1750
  Profile name
1367
1751
  </label>
1368
1752
  <div className="mt-1 flex gap-1.5">
@@ -1372,12 +1756,12 @@ function BrowserProfileMenu(props: {
1372
1756
  onInput={(event) => setName(event.currentTarget.value)}
1373
1757
  maxLength={200}
1374
1758
  placeholder="Work"
1375
- 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"
1759
+ 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"
1376
1760
  />
1377
1761
  <button
1378
1762
  type="submit"
1379
1763
  disabled={!name.trim() || props.saving}
1380
- 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"
1764
+ 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"
1381
1765
  >
1382
1766
  {props.saving ? (
1383
1767
  <LoaderCircleIcon className="size-3.5 animate-spin" />
@@ -1390,9 +1774,9 @@ function BrowserProfileMenu(props: {
1390
1774
  </form>
1391
1775
  )
1392
1776
  ) : (
1393
- <p className="mt-3 rounded-og-sm bg-og-surface-2 px-2.5 py-2 text-og-xs text-og-subtle">
1777
+ <p className="mt-3 rounded-og-sm bg-og-surface-2 px-2.5 py-2 text-og-xs text-og-fg-subtle">
1394
1778
  {attached
1395
- ? "Chrome keeps this profile's state directly; OpenGeni does not copy it automatically."
1779
+ ? "Chrome keeps this profile's state directly; Opengeni does not copy it automatically."
1396
1780
  : props.identity?.status === "archived"
1397
1781
  ? "Restore this profile before saving another version."
1398
1782
  : "This browser cannot save reusable profile state."}
@@ -1407,7 +1791,7 @@ function BrowserProfileMenu(props: {
1407
1791
  status: props.identity!.status === "active" ? "archived" : "active",
1408
1792
  })
1409
1793
  }
1410
- 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"
1794
+ 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"
1411
1795
  >
1412
1796
  {props.identity.status === "active" ? (
1413
1797
  <ArchiveIcon className="size-3.5" />
@@ -1418,8 +1802,9 @@ function BrowserProfileMenu(props: {
1418
1802
  </button>
1419
1803
  ) : null}
1420
1804
  {canSave ? (
1421
- <p className="mt-2 text-[10px] leading-4 text-og-subtle">
1422
- Saving briefly restarts this browser. Other open browsers are unchanged.
1805
+ <p className="mt-2 text-[10px] leading-4 text-og-fg-subtle">
1806
+ Saving restarts this browser. Save unfinished work on its pages first; unsaved form
1807
+ entries may be lost. Other open browsers are unchanged.
1423
1808
  </p>
1424
1809
  ) : null}
1425
1810
  </div>
@@ -1427,13 +1812,52 @@ function BrowserProfileMenu(props: {
1427
1812
  );
1428
1813
  }
1429
1814
 
1815
+ function revisionMaterializationSummary(revision: BrowserRevision): { label: string } {
1816
+ const materializations = revision.components.map((component) => component.materialization);
1817
+ const placementBound = materializations.find(
1818
+ (materialization) => materialization.portability === "placement_bound",
1819
+ );
1820
+ if (placementBound) {
1821
+ return {
1822
+ label: placementBound.reason
1823
+ ? `This machine only · ${placementBound.reason}`
1824
+ : "This machine only",
1825
+ };
1826
+ }
1827
+ const providerBound = materializations.find(
1828
+ (materialization) => materialization.portability === "provider_bound",
1829
+ );
1830
+ if (providerBound) {
1831
+ return {
1832
+ label: providerBound.reason ? `Provider-bound · ${providerBound.reason}` : "Provider-bound",
1833
+ };
1834
+ }
1835
+ return { label: "Portable browser data" };
1836
+ }
1837
+
1838
+ function siteAuthHealth(connection: SiteAuthConnection): {
1839
+ label: string;
1840
+ dotClass: string;
1841
+ } {
1842
+ switch (connection.verificationState) {
1843
+ case "verified":
1844
+ return { label: "Verified", dotClass: "bg-og-status-running" };
1845
+ case "needs_repair":
1846
+ return { label: "Sign-in needs attention", dotClass: "bg-og-status-waiting" };
1847
+ case "failed":
1848
+ return { label: "Verification failed", dotClass: "bg-og-status-error" };
1849
+ case "unknown":
1850
+ return { label: "Not checked yet", dotClass: "bg-og-muted" };
1851
+ }
1852
+ }
1853
+
1430
1854
  function MenuButton(props: { children: ReactNode; onClick: () => void; disabled?: boolean }) {
1431
1855
  return (
1432
1856
  <button
1433
1857
  type="button"
1434
1858
  onClick={props.onClick}
1435
1859
  disabled={props.disabled}
1436
- 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"
1860
+ 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"
1437
1861
  >
1438
1862
  {props.children}
1439
1863
  </button>
@@ -1451,7 +1875,7 @@ function BrowserTabs(props: {
1451
1875
  }) {
1452
1876
  const pages = props.targets.filter((target) => target.kind === "page" || target.kind === "popup");
1453
1877
  return (
1454
- <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">
1878
+ <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">
1455
1879
  {pages.map((target) => (
1456
1880
  <div
1457
1881
  key={target.id}
@@ -1459,7 +1883,7 @@ function BrowserTabs(props: {
1459
1883
  "group flex h-8 min-w-24 max-w-48 items-center rounded-t-og-sm border border-b-0 border-transparent px-2",
1460
1884
  target.id === props.selectedTargetId
1461
1885
  ? "border-og-border bg-og-bg text-og-fg"
1462
- : "text-og-muted hover:bg-og-surface-1",
1886
+ : "text-og-fg-muted hover:bg-og-surface-1",
1463
1887
  )}
1464
1888
  >
1465
1889
  <button
@@ -1487,7 +1911,7 @@ function BrowserTabs(props: {
1487
1911
  type="button"
1488
1912
  onClick={props.onOpen}
1489
1913
  disabled={props.mutating}
1490
- 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"
1914
+ 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"
1491
1915
  aria-label="New tab"
1492
1916
  >
1493
1917
  <PlusIcon className="size-3.5" />
@@ -1501,34 +1925,73 @@ function BrowserAddressBar(props: {
1501
1925
  target: BrowserTarget | null;
1502
1926
  loading: boolean;
1503
1927
  onNavigate: (url: string) => void;
1928
+ onHistory: (direction: "back" | "forward") => Promise<void>;
1504
1929
  onReload: () => void;
1505
1930
  }) {
1506
1931
  const [draft, setDraft] = useState(props.target?.url ?? "");
1507
1932
  const focusedRef = useRef(false);
1933
+ const historyPendingRef = useRef(false);
1934
+ const [historyPending, setHistoryPending] = useState(false);
1508
1935
  useEffect(() => {
1509
1936
  if (!focusedRef.current) setDraft(props.target?.url ?? "");
1510
1937
  }, [props.target?.id, props.target?.url]);
1511
1938
  const submit = (event: FormEvent) => {
1512
1939
  event.preventDefault();
1940
+ if (historyPendingRef.current) return;
1513
1941
  const normalized = normalizeBrowserAddress(draft);
1514
1942
  if (normalized) props.onNavigate(normalized);
1515
1943
  };
1944
+ const navigateHistory = async (direction: "back" | "forward") => {
1945
+ if (!props.target || props.loading || historyPendingRef.current) return;
1946
+ historyPendingRef.current = true;
1947
+ setHistoryPending(true);
1948
+ try {
1949
+ await props.onHistory(direction);
1950
+ } finally {
1951
+ historyPendingRef.current = false;
1952
+ setHistoryPending(false);
1953
+ }
1954
+ };
1516
1955
  return (
1517
1956
  <form
1518
1957
  onSubmit={submit}
1958
+ aria-busy={historyPending || undefined}
1519
1959
  className="flex h-10 shrink-0 items-center gap-1.5 border-b border-og-border bg-og-bg px-2"
1520
1960
  >
1961
+ {(["back", "forward"] as const).map((direction) => (
1962
+ <button
1963
+ key={direction}
1964
+ type="button"
1965
+ onClick={() => void navigateHistory(direction)}
1966
+ disabled={!props.target || props.loading}
1967
+ aria-disabled={historyPending || undefined}
1968
+ className={cn(
1969
+ "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",
1970
+ historyPending && "opacity-50",
1971
+ )}
1972
+ aria-label={direction === "back" ? "Back" : "Forward"}
1973
+ title={direction === "back" ? "Back" : "Forward"}
1974
+ >
1975
+ {direction === "back" ? (
1976
+ <ArrowLeftIcon className="size-3.5" />
1977
+ ) : (
1978
+ <ArrowRightIcon className="size-3.5" />
1979
+ )}
1980
+ </button>
1981
+ ))}
1521
1982
  <button
1522
1983
  type="button"
1523
1984
  onClick={props.onReload}
1524
- disabled={!props.target || props.loading}
1525
- 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"
1985
+ disabled={!props.target || props.loading || historyPending}
1986
+ 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"
1526
1987
  aria-label="Reload"
1527
1988
  >
1528
- <RefreshCwIcon className={cn("size-3.5", props.loading && "animate-spin")} />
1989
+ <RefreshCwIcon
1990
+ className={cn("size-3.5", (props.loading || historyPending) && "animate-spin")}
1991
+ />
1529
1992
  </button>
1530
- <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">
1531
- <Globe2Icon className="size-3 shrink-0 text-og-subtle" />
1993
+ <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">
1994
+ <Globe2Icon className="size-3 shrink-0 text-og-fg-subtle" />
1532
1995
  <input
1533
1996
  value={draft}
1534
1997
  onChange={(event) => setDraft(event.target.value)}
@@ -1541,11 +2004,12 @@ function BrowserAddressBar(props: {
1541
2004
  onKeyDown={(event) => {
1542
2005
  if (event.key !== "Enter") return;
1543
2006
  event.preventDefault();
2007
+ if (historyPendingRef.current) return;
1544
2008
  const normalized = normalizeBrowserAddress(draft);
1545
2009
  if (normalized) props.onNavigate(normalized);
1546
2010
  }}
1547
2011
  disabled={!props.target}
1548
- 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"
2012
+ 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"
1549
2013
  aria-label="Address"
1550
2014
  spellCheck={false}
1551
2015
  placeholder="Search or enter address"
@@ -1556,53 +2020,107 @@ function BrowserAddressBar(props: {
1556
2020
  }
1557
2021
 
1558
2022
  function BrowserViewport(props: {
1559
- frame: BrowserFrame | null;
2023
+ focusHandoffRef: { current: string | null };
2024
+ focusScope: string;
2025
+ frame: BrowserViewportFrame | null;
1560
2026
  connectionState: string;
1561
2027
  supportsLiveFrames: boolean;
1562
2028
  connectionError: Error | null;
2029
+ controlUnavailable: boolean;
1563
2030
  observation: ReturnType<typeof useBrowserSession>["observation"];
1564
2031
  mutating: boolean;
1565
2032
  clipboardEnabled: boolean;
2033
+ inputBatchAttachment?: {
2034
+ browserSessionId: string;
2035
+ controllerGeneration: string;
2036
+ targetId: string;
2037
+ fencedInputBatches?: true | undefined;
2038
+ focusedInputObservations?: true | undefined;
2039
+ expiresAt: string;
2040
+ } | null;
1566
2041
  activityLabel?: string | undefined;
1567
- onAction: (action: BrowserAction, frame: BrowserFrame | null) => Promise<BrowserActionReceipt>;
2042
+ onAction: (
2043
+ action: BrowserAction | BrowserActionBatch,
2044
+ frame: BrowserFrameInputFence | null,
2045
+ ) => Promise<BrowserActionReceipt>;
1568
2046
  onReadClipboard: () => Promise<{ text: string }>;
2047
+ onObserveForInput: () => Promise<BrowserObservation>;
2048
+ onSelectFromObservation: (
2049
+ action: BrowserAction,
2050
+ observation: BrowserObservation,
2051
+ ) => Promise<BrowserActionReceipt>;
1569
2052
  onReconnect: () => void;
2053
+ reconnectLabel?: string | undefined;
2054
+ reconnectMessage?: string | undefined;
1570
2055
  onError: (cause: unknown) => void;
1571
2056
  }) {
1572
2057
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
1573
2058
  const inputRef = useRef<HTMLTextAreaElement | null>(null);
2059
+ const [selectPopup, setSelectPopup] = useState<{
2060
+ observation: BrowserObservation | null;
2061
+ anchor: { x: number; y: number } | null;
2062
+ } | null>(null);
2063
+ const inputSequenceRef = useRef(0);
2064
+ const dismissSelectPopup = useCallback(() => {
2065
+ inputSequenceRef.current += 1;
2066
+ setSelectPopup({ observation: null, anchor: null });
2067
+ }, []);
1574
2068
  const composingRef = useRef(false);
1575
2069
  const pointerStartRef = useRef<PointerStart | null>(null);
1576
2070
  const lastClickRef = useRef<{
1577
2071
  at: number;
1578
2072
  x: number;
1579
2073
  y: number;
1580
- frame: BrowserFrame;
2074
+ frame: BrowserViewportFrame;
1581
2075
  } | null>(null);
1582
2076
  const wheelRef = useRef<{
1583
2077
  x: number;
1584
2078
  y: number;
1585
2079
  deltaX: number;
1586
2080
  deltaY: number;
1587
- frame: BrowserFrame;
2081
+ frame: BrowserViewportFrame;
1588
2082
  timer: ReturnType<typeof setTimeout> | null;
1589
2083
  } | null>(null);
1590
2084
  const pendingTextRef = useRef<{
1591
2085
  text: string;
1592
- frame: BrowserFrame | null;
2086
+ frame: BrowserViewportFrame | null;
1593
2087
  timer: ReturnType<typeof setTimeout>;
1594
2088
  } | null>(null);
1595
2089
  const actionRef = useRef(props.onAction);
1596
2090
  const readClipboardRef = useRef(props.onReadClipboard);
2091
+ const observeInputRef = useRef(props.onObserveForInput);
1597
2092
  const errorRef = useRef(props.onError);
1598
2093
  const actionTailRef = useRef<Promise<void>>(Promise.resolve());
1599
- const queuedFrameRef = useRef<BrowserFrame | null>(null);
2094
+ const actionQueueEpochRef = useRef(0);
2095
+ const queuedTypingRef = useRef<{
2096
+ actions: BrowserAction[];
2097
+ frame: BrowserFrameInputFence;
2098
+ epoch: number;
2099
+ } | null>(null);
2100
+ const queuedFrameRef = useRef<BrowserViewportFrame | null>(null);
2101
+ const currentFrameRef = useRef<BrowserViewportFrame | null>(props.frame);
2102
+ const paintedFrameRef = useRef<BrowserViewportFrame | null>(null);
2103
+ const [paintedFrame, setPaintedFrame] = useState<BrowserViewportFrame | null>(null);
1600
2104
  const decodingFrameRef = useRef(false);
1601
2105
  const mountedRef = useRef(true);
1602
2106
  actionRef.current = props.onAction;
1603
2107
  readClipboardRef.current = props.onReadClipboard;
2108
+ observeInputRef.current = props.onObserveForInput;
1604
2109
  errorRef.current = props.onError;
1605
2110
 
2111
+ const clearBufferedInput = useCallback(() => {
2112
+ dismissSelectPopup();
2113
+ if (wheelRef.current?.timer) clearTimeout(wheelRef.current.timer);
2114
+ if (pendingTextRef.current?.timer) clearTimeout(pendingTextRef.current.timer);
2115
+ wheelRef.current = null;
2116
+ pendingTextRef.current = null;
2117
+ queuedTypingRef.current = null;
2118
+ pointerStartRef.current = null;
2119
+ lastClickRef.current = null;
2120
+ composingRef.current = false;
2121
+ if (inputRef.current) inputRef.current.value = "";
2122
+ }, [dismissSelectPopup]);
2123
+
1606
2124
  const paintQueuedFrames = useCallback(() => {
1607
2125
  if (decodingFrameRef.current) return;
1608
2126
  decodingFrameRef.current = true;
@@ -1615,15 +2133,22 @@ function BrowserViewport(props: {
1615
2133
 
1616
2134
  let objectUrl: string | null = null;
1617
2135
  try {
1618
- const blob = new Blob([frame.data.slice().buffer], {
2136
+ const blob = new Blob([frame.data], {
1619
2137
  type: frame.mediaType,
1620
2138
  });
1621
2139
  if (typeof createImageBitmap === "function") {
1622
2140
  const bitmap = await createImageBitmap(blob);
1623
2141
  try {
1624
2142
  const canvas = canvasRef.current;
1625
- if (mountedRef.current && canvas) {
2143
+ if (
2144
+ mountedRef.current &&
2145
+ canvas &&
2146
+ currentFrameRef.current &&
2147
+ sameBrowserDocument(frame, currentFrameRef.current)
2148
+ ) {
1626
2149
  paintCanvas(canvas, bitmap, frame.width, frame.height);
2150
+ paintedFrameRef.current = frame;
2151
+ setPaintedFrame(frame);
1627
2152
  }
1628
2153
  } finally {
1629
2154
  bitmap.close();
@@ -1633,11 +2158,23 @@ function BrowserViewport(props: {
1633
2158
  objectUrl = URL.createObjectURL(blob);
1634
2159
  const image = await loadImage(objectUrl);
1635
2160
  const canvas = canvasRef.current;
1636
- if (mountedRef.current && canvas) {
2161
+ if (
2162
+ mountedRef.current &&
2163
+ canvas &&
2164
+ currentFrameRef.current &&
2165
+ sameBrowserDocument(frame, currentFrameRef.current)
2166
+ ) {
1637
2167
  paintCanvas(canvas, image, frame.width, frame.height);
2168
+ paintedFrameRef.current = frame;
2169
+ setPaintedFrame(frame);
1638
2170
  }
1639
2171
  } catch (cause) {
1640
- if (mountedRef.current) errorRef.current(cause);
2172
+ if (
2173
+ mountedRef.current &&
2174
+ currentFrameRef.current &&
2175
+ sameBrowserDocument(frame, currentFrameRef.current)
2176
+ )
2177
+ errorRef.current(cause);
1641
2178
  } finally {
1642
2179
  if (objectUrl) URL.revokeObjectURL(objectUrl);
1643
2180
  }
@@ -1649,47 +2186,135 @@ function BrowserViewport(props: {
1649
2186
  })();
1650
2187
  }, []);
1651
2188
 
1652
- useEffect(() => {
2189
+ useLayoutEffect(() => {
2190
+ const keyboardInput = inputRef.current;
2191
+ const focusHandoff = props.focusHandoffRef;
1653
2192
  mountedRef.current = true;
2193
+ if (focusHandoff.current === props.focusScope) {
2194
+ keyboardInput?.focus({ preventScroll: true });
2195
+ }
2196
+ focusHandoff.current = null;
1654
2197
  return () => {
2198
+ // Only a same-target replacement may inherit focus the keyboard sink owned.
2199
+ if (document.activeElement === keyboardInput) {
2200
+ focusHandoff.current = props.focusScope;
2201
+ }
1655
2202
  mountedRef.current = false;
2203
+ actionQueueEpochRef.current += 1;
1656
2204
  queuedFrameRef.current = null;
2205
+ clearBufferedInput();
1657
2206
  };
1658
- }, []);
2207
+ }, [clearBufferedInput, props.focusHandoffRef, props.focusScope]);
1659
2208
 
1660
- useEffect(() => {
1661
- if (!props.frame) return;
2209
+ useLayoutEffect(() => {
2210
+ if (currentFrameRef.current && !props.frame) {
2211
+ actionQueueEpochRef.current += 1;
2212
+ clearBufferedInput();
2213
+ }
2214
+ currentFrameRef.current = props.frame;
2215
+ if (!props.frame) {
2216
+ queuedFrameRef.current = null;
2217
+ paintedFrameRef.current = null;
2218
+ setPaintedFrame(null);
2219
+ return;
2220
+ }
1662
2221
  queuedFrameRef.current = props.frame;
1663
2222
  paintQueuedFrames();
1664
- }, [paintQueuedFrames, props.frame]);
1665
-
1666
- useEffect(
1667
- () => () => {
1668
- if (wheelRef.current?.timer) clearTimeout(wheelRef.current.timer);
1669
- if (pendingTextRef.current?.timer) clearTimeout(pendingTextRef.current.timer);
1670
- },
1671
- [],
1672
- );
2223
+ }, [clearBufferedInput, paintQueuedFrames, props.frame]);
1673
2224
 
1674
2225
  const enqueue = useCallback(
1675
2226
  (
1676
2227
  action: BrowserAction,
1677
- frame: BrowserFrame | null,
2228
+ frame: BrowserFrameInputFence | null,
1678
2229
  after?: (receipt: BrowserActionReceipt) => Promise<void>,
2230
+ selectAnchor?: { x: number; y: number },
1679
2231
  ) => {
2232
+ const inputSequence = ++inputSequenceRef.current;
2233
+ setSelectPopup({ observation: null, anchor: null });
2234
+ // Detach image bytes even when the caller supplied a complete painted frame.
2235
+ frame = frame
2236
+ ? {
2237
+ browserSessionId: frame.browserSessionId,
2238
+ controllerGeneration: frame.controllerGeneration,
2239
+ targetId: frame.targetId,
2240
+ targetGeneration: frame.targetGeneration,
2241
+ documentGeneration: frame.documentGeneration,
2242
+ frameId: frame.frameId,
2243
+ }
2244
+ : null;
2245
+ const epoch = actionQueueEpochRef.current;
2246
+ const attachment = props.inputBatchAttachment;
2247
+ const canBatchTyping =
2248
+ action.type === "type" &&
2249
+ !after &&
2250
+ frame &&
2251
+ attachment?.fencedInputBatches === true &&
2252
+ attachment.browserSessionId === frame.browserSessionId &&
2253
+ attachment.controllerGeneration === frame.controllerGeneration &&
2254
+ attachment.targetId === frame.targetId &&
2255
+ Date.parse(attachment.expiresAt) > Date.now();
2256
+ const previous = queuedTypingRef.current;
2257
+ if (
2258
+ canBatchTyping &&
2259
+ frame &&
2260
+ previous &&
2261
+ previous.epoch === epoch &&
2262
+ sameFrameFence(previous.frame, frame) &&
2263
+ previous.actions.length < 16
2264
+ ) {
2265
+ // Keep every existing text/input event boundary. Only share transport.
2266
+ previous.actions.push(action);
2267
+ return;
2268
+ }
2269
+ const typing = canBatchTyping && frame ? { actions: [action], frame, epoch } : null;
2270
+ // Every non-typing action is an ordering barrier, including keys and paste.
2271
+ queuedTypingRef.current = typing;
1680
2272
  actionTailRef.current = actionTailRef.current
1681
2273
  .catch(() => undefined)
1682
2274
  .then(async () => {
1683
- const receipt = await actionRef.current(action, frame);
2275
+ if (!mountedRef.current || epoch !== actionQueueEpochRef.current) return;
2276
+ if (queuedTypingRef.current === typing) queuedTypingRef.current = null;
2277
+ const dispatchedAction =
2278
+ typing && typing.actions.length > 1
2279
+ ? { type: "batch" as const, actions: typing.actions, fenceEachAction: true as const }
2280
+ : action;
2281
+ // Read after the epoch guard: a queued render callback can retain every
2282
+ // frame from that render even when its input fence contains no bytes.
2283
+ const receipt = await actionRef.current(dispatchedAction, frame);
2284
+ if (!mountedRef.current || epoch !== actionQueueEpochRef.current) return;
2285
+ // Alt+Down explicitly opens a native dropdown from the keyboard.
2286
+ // Existing controllers return focused-input metadata for clicks only,
2287
+ // so use the ordinary authorized read for this explicit gesture.
2288
+ const keyboardSelect =
2289
+ dispatchedAction.type === "press" && dispatchedAction.key === "Alt+ArrowDown";
2290
+ if ((selectAnchor || keyboardSelect) && inputSequence === inputSequenceRef.current) {
2291
+ const observation = keyboardSelect
2292
+ ? await observeInputRef.current()
2293
+ : receipt.observation;
2294
+ if (
2295
+ mountedRef.current &&
2296
+ epoch === actionQueueEpochRef.current &&
2297
+ inputSequence === inputSequenceRef.current &&
2298
+ observation
2299
+ ) {
2300
+ setSelectPopup({ observation, anchor: selectAnchor ?? null });
2301
+ }
2302
+ }
1684
2303
  await after?.(receipt);
1685
2304
  })
1686
- .catch((cause) => errorRef.current(cause));
2305
+ .catch((cause) => {
2306
+ if (!mountedRef.current || epoch !== actionQueueEpochRef.current) return;
2307
+ // Do not replay or continue input collected behind a failed request.
2308
+ actionQueueEpochRef.current += 1;
2309
+ clearBufferedInput();
2310
+ errorRef.current(cause);
2311
+ });
1687
2312
  },
1688
- [],
2313
+ [clearBufferedInput, props.inputBatchAttachment],
1689
2314
  );
1690
2315
 
1691
2316
  const point = useCallback(
1692
- (frame: BrowserFrame, clientX: number, clientY: number) =>
2317
+ (frame: BrowserViewportFrame, clientX: number, clientY: number) =>
1693
2318
  browserPoint(canvasRef.current, frame, clientX, clientY),
1694
2319
  [],
1695
2320
  );
@@ -1706,19 +2331,40 @@ function BrowserViewport(props: {
1706
2331
  lastClickRef.current = null;
1707
2332
  }, []);
1708
2333
 
2334
+ const flushPendingWheel = useCallback(() => {
2335
+ const batch = wheelRef.current;
2336
+ if (!batch) return;
2337
+ if (batch.timer) clearTimeout(batch.timer);
2338
+ wheelRef.current = null;
2339
+ enqueue(
2340
+ {
2341
+ type: "pointer",
2342
+ action: "scroll",
2343
+ x: batch.x,
2344
+ y: batch.y,
2345
+ deltaX: batch.deltaX,
2346
+ deltaY: batch.deltaY,
2347
+ },
2348
+ batch.frame,
2349
+ );
2350
+ }, [enqueue]);
2351
+
1709
2352
  const pointerDown = (event: PointerEvent<HTMLCanvasElement>) => {
1710
- if (!props.frame || event.button !== 0) return;
2353
+ const frame = paintedFrameRef.current;
2354
+ if (!frame || event.button !== 0) return;
1711
2355
  // Keep the hidden keyboard sink focused after the browser performs the
1712
2356
  // pointerdown default action for the canvas. Without preventing that
1713
2357
  // default, Chrome immediately moves focus back to the document and all
1714
2358
  // subsequent typing/paste is silently lost.
1715
2359
  event.preventDefault();
2360
+ dismissSelectPopup();
2361
+ flushPendingWheel();
1716
2362
  flushPendingText();
1717
2363
  pointerStartRef.current = {
1718
2364
  x: event.clientX,
1719
2365
  y: event.clientY,
1720
2366
  pointerId: event.pointerId,
1721
- frame: props.frame,
2367
+ frame,
1722
2368
  };
1723
2369
  event.currentTarget.setPointerCapture?.(event.pointerId);
1724
2370
  inputRef.current?.focus({ preventScroll: true });
@@ -1762,13 +2408,22 @@ function BrowserViewport(props: {
1762
2408
  return;
1763
2409
  }
1764
2410
  lastClickRef.current = { at: now, x: to.x, y: to.y, frame: start.frame };
1765
- enqueue({ type: "pointer", action: "click", x: to.x, y: to.y }, start.frame);
2411
+ const viewportBounds = canvasRef.current?.parentElement?.getBoundingClientRect();
2412
+ enqueue(
2413
+ { type: "pointer", action: "click", x: to.x, y: to.y },
2414
+ start.frame,
2415
+ undefined,
2416
+ viewportBounds
2417
+ ? { x: event.clientX - viewportBounds.left, y: event.clientY - viewportBounds.top + 8 }
2418
+ : undefined,
2419
+ );
1766
2420
  };
1767
2421
 
1768
2422
  const contextMenu = (event: MouseEvent<HTMLCanvasElement>) => {
1769
2423
  event.preventDefault();
1770
- const frame = props.frame;
2424
+ const frame = paintedFrameRef.current;
1771
2425
  if (!frame) return;
2426
+ flushPendingWheel();
1772
2427
  flushPendingText();
1773
2428
  flushPendingClick();
1774
2429
  const at = point(frame, event.clientX, event.clientY);
@@ -1776,41 +2431,47 @@ function BrowserViewport(props: {
1776
2431
  };
1777
2432
 
1778
2433
  const wheel = (event: WheelEvent<HTMLCanvasElement>) => {
1779
- const frame = props.frame;
2434
+ dismissSelectPopup();
2435
+ const frame = paintedFrameRef.current;
1780
2436
  if (!frame) return;
1781
2437
  const at = point(frame, event.clientX, event.clientY);
1782
2438
  if (!at) return;
1783
2439
  flushPendingText();
1784
2440
  flushPendingClick();
1785
2441
  event.preventDefault();
1786
- const pending = wheelRef.current;
1787
- if (pending?.timer) clearTimeout(pending.timer);
2442
+ let pending = wheelRef.current;
2443
+ if (
2444
+ pending &&
2445
+ (!sameBrowserDocument(pending.frame, frame) ||
2446
+ Math.hypot(pending.x - at.x, pending.y - at.y) > 6)
2447
+ ) {
2448
+ flushPendingWheel();
2449
+ pending = null;
2450
+ }
1788
2451
  wheelRef.current = {
1789
2452
  x: at.x,
1790
2453
  y: at.y,
1791
- deltaX: (pending && sameFrameFence(pending.frame, frame) ? pending.deltaX : 0) + event.deltaX,
1792
- deltaY: (pending && sameFrameFence(pending.frame, frame) ? pending.deltaY : 0) + event.deltaY,
2454
+ deltaX: (pending?.deltaX ?? 0) + event.deltaX,
2455
+ deltaY: (pending?.deltaY ?? 0) + event.deltaY,
1793
2456
  frame,
1794
- timer: setTimeout(() => {
1795
- const batch = wheelRef.current;
1796
- wheelRef.current = null;
1797
- if (batch)
1798
- enqueue(
1799
- {
1800
- type: "pointer",
1801
- action: "scroll",
1802
- x: batch.x,
1803
- y: batch.y,
1804
- deltaX: batch.deltaX,
1805
- deltaY: batch.deltaY,
1806
- },
1807
- batch.frame,
1808
- );
1809
- }, 45),
2457
+ // Retain the first event's deadline so a continuous gesture keeps moving.
2458
+ timer: pending?.timer ?? setTimeout(flushPendingWheel, 45),
1810
2459
  };
1811
2460
  };
1812
2461
 
1813
2462
  const keyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
2463
+ // Candidate selection/editing belongs to the local IME. Forward only the
2464
+ // committed text; Enter must not submit the remote page during composition.
2465
+ // Safari can end composition before keydown while retaining keyCode 229.
2466
+ if (
2467
+ composingRef.current ||
2468
+ event.nativeEvent.isComposing ||
2469
+ event.nativeEvent.keyCode === 229
2470
+ ) {
2471
+ return;
2472
+ }
2473
+ dismissSelectPopup();
2474
+ flushPendingWheel();
1814
2475
  const command = event.metaKey || event.ctrlKey;
1815
2476
  if (
1816
2477
  props.clipboardEnabled &&
@@ -1826,26 +2487,31 @@ function BrowserViewport(props: {
1826
2487
  event.preventDefault();
1827
2488
  flushPendingText();
1828
2489
  flushPendingClick();
1829
- enqueue({ type: "press", key }, props.frame);
2490
+ enqueue({ type: "press", key }, paintedFrameRef.current);
1830
2491
  };
1831
2492
 
2493
+ const finishCopy = useCallback(async () => {
2494
+ const clipboard = await readClipboardRef.current();
2495
+ if (!mountedRef.current) return;
2496
+ if (clipboard.text.length === 0) return;
2497
+ if (!(await copyTextToClipboard(clipboard.text))) {
2498
+ throw new Error("Browser text could not be copied to the local clipboard");
2499
+ }
2500
+ }, []);
2501
+
1832
2502
  const copy = (event: ClipboardEvent<HTMLTextAreaElement>) => {
1833
2503
  if (!props.clipboardEnabled) return;
1834
2504
  event.preventDefault();
2505
+ flushPendingWheel();
1835
2506
  flushPendingText();
1836
2507
  flushPendingClick();
1837
- enqueue({ type: "clipboard", operation: "copy" }, props.frame, async () => {
1838
- const clipboard = await readClipboardRef.current();
1839
- if (clipboard.text.length === 0) return;
1840
- if (!(await copyTextToClipboard(clipboard.text))) {
1841
- throw new Error("Browser text could not be copied to the local clipboard");
1842
- }
1843
- });
2508
+ enqueue({ type: "clipboard", operation: "copy" }, paintedFrameRef.current, finishCopy);
1844
2509
  };
1845
2510
 
1846
2511
  const paste = (event: ClipboardEvent<HTMLTextAreaElement>) => {
1847
2512
  if (!props.clipboardEnabled) return;
1848
2513
  event.preventDefault();
2514
+ flushPendingWheel();
1849
2515
  flushPendingText();
1850
2516
  flushPendingClick();
1851
2517
  enqueue(
@@ -1854,16 +2520,18 @@ function BrowserViewport(props: {
1854
2520
  operation: "paste",
1855
2521
  text: event.clipboardData.getData("text/plain"),
1856
2522
  },
1857
- props.frame,
2523
+ paintedFrameRef.current,
1858
2524
  );
1859
2525
  };
1860
2526
 
1861
2527
  const input = (value: string, nativeComposing = false) => {
2528
+ dismissSelectPopup();
1862
2529
  if (composingRef.current || nativeComposing) return;
1863
2530
  if (!value) return;
2531
+ flushPendingWheel();
1864
2532
  flushPendingClick();
1865
2533
  const pending = pendingTextRef.current;
1866
- if (pending && sameOptionalBrowserFrame(pending.frame, props.frame)) {
2534
+ if (pending && sameOptionalBrowserFrame(pending.frame, paintedFrameRef.current)) {
1867
2535
  clearTimeout(pending.timer);
1868
2536
  pending.text += value;
1869
2537
  pending.timer = setTimeout(flushPendingText, 16);
@@ -1871,7 +2539,7 @@ function BrowserViewport(props: {
1871
2539
  flushPendingText();
1872
2540
  pendingTextRef.current = {
1873
2541
  text: value,
1874
- frame: props.frame,
2542
+ frame: paintedFrameRef.current,
1875
2543
  timer: setTimeout(flushPendingText, 16),
1876
2544
  };
1877
2545
  }
@@ -1887,7 +2555,11 @@ function BrowserViewport(props: {
1887
2555
  input(event.currentTarget.value || event.data);
1888
2556
  };
1889
2557
 
1890
- const showCanvas = props.frame !== null;
2558
+ const showCanvas =
2559
+ !props.controlUnavailable &&
2560
+ props.frame !== null &&
2561
+ paintedFrame !== null &&
2562
+ sameBrowserDocument(props.frame, paintedFrame);
1891
2563
  return (
1892
2564
  <div className="relative min-h-0 flex-1 overflow-hidden bg-black">
1893
2565
  <canvas
@@ -1907,6 +2579,7 @@ function BrowserViewport(props: {
1907
2579
  />
1908
2580
  <textarea
1909
2581
  ref={inputRef}
2582
+ disabled={props.controlUnavailable}
1910
2583
  defaultValue=""
1911
2584
  onInput={(event) =>
1912
2585
  input(event.currentTarget.value, (event.nativeEvent as InputEvent).isComposing)
@@ -1918,6 +2591,7 @@ function BrowserViewport(props: {
1918
2591
  onPaste={paste}
1919
2592
  className="pointer-events-none absolute left-1/2 top-1/2 size-px resize-none overflow-hidden opacity-0"
1920
2593
  aria-label="Browser keyboard input"
2594
+ aria-keyshortcuts="Alt+ArrowDown"
1921
2595
  autoCapitalize="off"
1922
2596
  autoCorrect="off"
1923
2597
  spellCheck={false}
@@ -1928,8 +2602,26 @@ function BrowserViewport(props: {
1928
2602
  connectionState={props.connectionState}
1929
2603
  supportsLiveFrames={props.supportsLiveFrames}
1930
2604
  error={props.connectionError}
2605
+ controlUnavailable={props.controlUnavailable}
1931
2606
  onAction={(action) => enqueue(action, null)}
1932
2607
  onReconnect={props.onReconnect}
2608
+ reconnectLabel={props.reconnectLabel}
2609
+ reconnectMessage={props.reconnectMessage}
2610
+ />
2611
+ ) : null}
2612
+ {showCanvas ? (
2613
+ <BrowserSelectControl
2614
+ manualFallback={props.inputBatchAttachment?.focusedInputObservations !== true}
2615
+ activation={selectPopup}
2616
+ onDismiss={() => inputRef.current?.focus({ preventScroll: true })}
2617
+ observe={async () => {
2618
+ flushPendingText();
2619
+ flushPendingWheel();
2620
+ await actionTailRef.current;
2621
+ if (!mountedRef.current) throw new Error("The browser page changed.");
2622
+ return await props.onObserveForInput();
2623
+ }}
2624
+ act={props.onSelectFromObservation}
1933
2625
  />
1934
2626
  ) : null}
1935
2627
  {props.mutating ? (
@@ -1946,35 +2638,52 @@ function SemanticBrowserFallback(props: {
1946
2638
  connectionState: string;
1947
2639
  supportsLiveFrames: boolean;
1948
2640
  error: Error | null;
2641
+ controlUnavailable: boolean;
1949
2642
  onAction: (action: BrowserAction) => void;
1950
2643
  onReconnect: () => void;
2644
+ reconnectLabel?: string | undefined;
2645
+ reconnectMessage?: string | undefined;
1951
2646
  }) {
2647
+ const controlFailure = interactionControlFailureFromError(props.error);
2648
+ const { controlUnavailable } = props;
2649
+ const generationLoss = Boolean(props.reconnectMessage);
1952
2650
  const nodes = semanticNodes(
1953
2651
  props.observation?.semantic?.kind === "snapshot" ? props.observation.semantic.roots : [],
1954
2652
  );
1955
2653
  const interactive = nodes.filter((node) => node.actions.includes("click")).slice(0, 8);
1956
2654
  return (
1957
2655
  <div className="absolute inset-0 grid place-items-center bg-og-bg p-6">
1958
- <div className="w-full max-w-md rounded-og-lg border border-og-border bg-og-surface-0 p-4 shadow-lg">
2656
+ <div className="w-full max-w-md rounded-og-lg border border-og-border bg-og-surface-1 p-4 shadow-lg">
1959
2657
  <div className="flex items-center gap-2">
1960
2658
  {props.error ? (
1961
- <CircleAlertIcon className="size-4 text-og-status-error" />
1962
- ) : !props.supportsLiveFrames ? (
1963
- <ZapIcon className="size-4 text-og-muted" />
2659
+ <CircleAlertIcon className="size-4 text-og-danger" />
2660
+ ) : props.connectionState === "semantic" ? (
2661
+ <ZapIcon className="size-4 text-og-fg-muted" />
1964
2662
  ) : (
1965
- <LoaderCircleIcon className="size-4 animate-spin text-og-muted" />
2663
+ <LoaderCircleIcon className="size-4 animate-spin text-og-fg-muted" />
1966
2664
  )}
1967
2665
  <p className="text-og-menu font-medium text-og-fg">
1968
- {props.error
1969
- ? "Live view disconnected"
1970
- : props.supportsLiveFrames
1971
- ? browserConnectionLabel(props.connectionState)
1972
- : "Semantic browser"}
2666
+ {generationLoss
2667
+ ? "Chrome reconnected—open a fresh browser/desktop."
2668
+ : controlUnavailable
2669
+ ? "Browser controls unavailable"
2670
+ : props.error
2671
+ ? props.supportsLiveFrames
2672
+ ? "Live view disconnected"
2673
+ : "Browser unavailable"
2674
+ : props.connectionState === "semantic"
2675
+ ? "Semantic browser"
2676
+ : browserConnectionLabel(props.connectionState)}
1973
2677
  </p>
1974
2678
  </div>
1975
- {interactive.length > 0 ? (
2679
+ {props.error || props.reconnectMessage ? (
2680
+ <p className="mt-2 text-og-control leading-5 text-og-fg-muted">
2681
+ {props.reconnectMessage ?? controlFailure?.message ?? props.error?.message}
2682
+ </p>
2683
+ ) : null}
2684
+ {interactive.length > 0 && !controlUnavailable ? (
1976
2685
  <div className="mt-3 border-t border-og-border pt-3">
1977
- <p className="mb-2 text-og-xs text-og-subtle">
2686
+ <p className="mb-2 text-og-xs text-og-fg-subtle">
1978
2687
  {props.supportsLiveFrames
1979
2688
  ? "Page controls remain available"
1980
2689
  : "Available page controls"}
@@ -1998,13 +2707,14 @@ function SemanticBrowserFallback(props: {
1998
2707
  </div>
1999
2708
  </div>
2000
2709
  ) : null}
2001
- {props.error && props.supportsLiveFrames ? (
2710
+ {props.error && (!generationLoss || props.reconnectLabel) ? (
2002
2711
  <button
2003
2712
  type="button"
2004
2713
  onClick={props.onReconnect}
2005
2714
  className="mt-3 text-og-control font-medium text-og-accent hover:underline"
2006
2715
  >
2007
- Reconnect
2716
+ {props.reconnectLabel ??
2717
+ (controlFailure?.retryable === false ? "Try again" : "Reconnect")}
2008
2718
  </button>
2009
2719
  ) : null}
2010
2720
  </div>
@@ -2017,6 +2727,7 @@ function BrowserStatusBar(props: {
2017
2727
  profile: BrowserIdentity | null;
2018
2728
  target: BrowserTarget | null;
2019
2729
  observation: ReturnType<typeof useBrowserSession>["observation"];
2730
+ inputFailure: ReturnType<typeof useBrowserSession>["inputFailure"];
2020
2731
  connectionState: string;
2021
2732
  refreshing: boolean;
2022
2733
  diagnosticsOpen: boolean;
@@ -2028,7 +2739,7 @@ function BrowserStatusBar(props: {
2028
2739
  (props.observation?.diagnostics.pageErrorCount ?? 0);
2029
2740
  const failed = props.observation?.diagnostics.failedRequestCount ?? 0;
2030
2741
  return (
2031
- <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">
2742
+ <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">
2032
2743
  <span
2033
2744
  className={cn(
2034
2745
  "size-1.5 rounded-full",
@@ -2044,6 +2755,11 @@ function BrowserStatusBar(props: {
2044
2755
  ? "Semantic"
2045
2756
  : browserConnectionLabel(props.connectionState)}
2046
2757
  </span>
2758
+ {props.inputFailure ? (
2759
+ <span className="text-og-status-failed">
2760
+ {props.inputFailure.state === "failed" ? "Input failed" : "Input result unknown"}
2761
+ </span>
2762
+ ) : null}
2047
2763
  <span>{props.profile?.name ?? "Temporary browser"}</span>
2048
2764
  <span className="min-w-0 flex-1 truncate">{props.target?.title}</span>
2049
2765
  {props.refreshing ? <LoaderCircleIcon className="size-3 animate-spin" /> : null}
@@ -2052,10 +2768,10 @@ function BrowserStatusBar(props: {
2052
2768
  type="button"
2053
2769
  onClick={props.onOpenComputer}
2054
2770
  className="flex items-center gap-1 rounded px-1.5 py-0.5 transition hover:bg-og-surface-2 hover:text-og-fg"
2055
- aria-label="Open this browser window in Computer"
2771
+ aria-label="Open this browser window in Desktop"
2056
2772
  >
2057
2773
  <MonitorIcon className="size-3" />
2058
- Computer
2774
+ Desktop
2059
2775
  </button>
2060
2776
  ) : null}
2061
2777
  <button
@@ -2109,13 +2825,13 @@ function BrowserDiagnosticsDrawer(props: {
2109
2825
  id="browser-diagnostics-drawer"
2110
2826
  role="region"
2111
2827
  aria-label="Browser diagnostics"
2112
- 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"
2828
+ 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"
2113
2829
  >
2114
- <div className="flex h-11 shrink-0 items-center gap-2 border-b border-og-border px-3">
2115
- <BugIcon className="size-4 text-og-muted" />
2830
+ <div className="flex min-h-11 shrink-0 items-center gap-2 border-b border-og-border px-3 py-2">
2831
+ <BugIcon className="size-4 shrink-0 text-og-fg-muted" />
2116
2832
  <div className="min-w-0 flex-1">
2117
2833
  <p className="text-og-control font-semibold text-og-fg">Browser diagnostics</p>
2118
- <p className="truncate text-og-xs text-og-subtle">
2834
+ <p className="truncate text-og-xs text-og-fg-subtle">
2119
2835
  {props.target?.title || shortUrl(props.target?.url ?? "") || "Current tab"}
2120
2836
  </p>
2121
2837
  </div>
@@ -2123,7 +2839,7 @@ function BrowserDiagnosticsDrawer(props: {
2123
2839
  type="button"
2124
2840
  onClick={props.onRefresh}
2125
2841
  disabled={props.state.loading}
2126
- 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"
2842
+ 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"
2127
2843
  aria-label="Refresh browser diagnostics"
2128
2844
  >
2129
2845
  <RefreshCwIcon className={cn("size-3.5", props.state.loading && "animate-spin")} />
@@ -2131,7 +2847,7 @@ function BrowserDiagnosticsDrawer(props: {
2131
2847
  <button
2132
2848
  type="button"
2133
2849
  onClick={props.onClose}
2134
- className="grid size-7 place-items-center rounded-og-sm text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg"
2850
+ 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"
2135
2851
  aria-label="Close browser diagnostics"
2136
2852
  >
2137
2853
  <XIcon className="size-3.5" />
@@ -2142,7 +2858,7 @@ function BrowserDiagnosticsDrawer(props: {
2142
2858
  <section className="border-b border-og-border p-3" aria-labelledby="browser-runtime-title">
2143
2859
  <h3
2144
2860
  id="browser-runtime-title"
2145
- className="text-[10px] font-semibold uppercase tracking-[0.14em] text-og-subtle"
2861
+ className="text-[10px] font-semibold uppercase tracking-[0.14em] text-og-fg-subtle"
2146
2862
  >
2147
2863
  Runtime
2148
2864
  </h3>
@@ -2173,18 +2889,19 @@ function BrowserDiagnosticsDrawer(props: {
2173
2889
  <section className="border-b border-og-border p-3" aria-labelledby="browser-page-title">
2174
2890
  <h3
2175
2891
  id="browser-page-title"
2176
- className="text-[10px] font-semibold uppercase tracking-[0.14em] text-og-subtle"
2892
+ className="text-[10px] font-semibold uppercase tracking-[0.14em] text-og-fg-subtle"
2177
2893
  >
2178
- Current page
2894
+ Tab diagnostics
2179
2895
  </h3>
2896
+ <p className="mt-1 text-og-xs text-og-fg-subtle">Includes earlier pages in this tab.</p>
2180
2897
  <dl className="mt-2 grid grid-cols-2 gap-2">
2181
- <DiagnosticCount label="Console errors" value={diagnostics?.consoleErrorCount ?? 0} />
2182
- <DiagnosticCount label="Page errors" value={diagnostics?.pageErrorCount ?? 0} />
2183
- <DiagnosticCount label="Failed requests" value={diagnostics?.failedRequestCount ?? 0} />
2184
- <DiagnosticCount label="Downloads" value={diagnostics?.downloadCount ?? 0} />
2898
+ <DiagnosticCount label="Console errors" value={diagnostics?.consoleErrorCount} />
2899
+ <DiagnosticCount label="Page errors" value={diagnostics?.pageErrorCount} />
2900
+ <DiagnosticCount label="Failed requests" value={diagnostics?.failedRequestCount} />
2901
+ <DiagnosticCount label="Downloads" value={diagnostics?.downloadCount} />
2185
2902
  </dl>
2186
2903
  {session?.failureCode ? (
2187
- <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">
2904
+ <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">
2188
2905
  Browser state: {session.failureCode}
2189
2906
  </p>
2190
2907
  ) : null}
@@ -2206,21 +2923,21 @@ function BrowserDiagnosticsDrawer(props: {
2206
2923
  <div className="flex items-center justify-between gap-2">
2207
2924
  <h3
2208
2925
  id="browser-events-title"
2209
- className="text-[10px] font-semibold uppercase tracking-[0.14em] text-og-subtle"
2926
+ className="text-[10px] font-semibold uppercase tracking-[0.14em] text-og-fg-subtle"
2210
2927
  >
2211
2928
  Recent events
2212
2929
  </h3>
2213
2930
  {props.state.batch?.truncated ? (
2214
- <span className="text-[10px] text-og-subtle">Newest 100</span>
2931
+ <span className="text-[10px] text-og-fg-subtle">Newest 100</span>
2215
2932
  ) : null}
2216
2933
  </div>
2217
2934
  {props.state.loading && !props.state.batch ? (
2218
- <div className="flex items-center gap-2 py-6 text-og-control text-og-muted">
2935
+ <div className="flex items-center gap-2 py-6 text-og-control text-og-fg-muted">
2219
2936
  <LoaderCircleIcon className="size-3.5 animate-spin" /> Loading diagnostics…
2220
2937
  </div>
2221
2938
  ) : props.state.error ? (
2222
2939
  <div className="py-5">
2223
- <p className="text-og-control text-og-status-error">{props.state.error.message}</p>
2940
+ <p className="text-og-control text-og-danger">{props.state.error.message}</p>
2224
2941
  <button
2225
2942
  type="button"
2226
2943
  onClick={props.onRefresh}
@@ -2236,7 +2953,7 @@ function BrowserDiagnosticsDrawer(props: {
2236
2953
  key={entry.sequence}
2237
2954
  className="rounded-og-sm border border-og-border bg-og-bg p-2.5"
2238
2955
  >
2239
- <div className="flex items-center gap-2 text-[10px] text-og-subtle">
2956
+ <div className="flex items-center gap-2 text-[10px] text-og-fg-subtle">
2240
2957
  <span
2241
2958
  className={cn(
2242
2959
  "font-semibold uppercase tracking-wide",
@@ -2251,7 +2968,7 @@ function BrowserDiagnosticsDrawer(props: {
2251
2968
  {entry.message}
2252
2969
  </p>
2253
2970
  {entry.url || entry.filename || entry.method || entry.status ? (
2254
- <p className="mt-1 truncate font-og-mono text-[10px] text-og-subtle">
2971
+ <p className="mt-1 truncate font-og-mono text-[10px] text-og-fg-subtle">
2255
2972
  {[entry.method, entry.status, entry.filename, entry.url]
2256
2973
  .filter((value) => value !== null)
2257
2974
  .join(" · ")}
@@ -2261,7 +2978,7 @@ function BrowserDiagnosticsDrawer(props: {
2261
2978
  ))}
2262
2979
  </ol>
2263
2980
  ) : (
2264
- <p className="py-6 text-og-control text-og-muted">No diagnostics for this tab.</p>
2981
+ <p className="py-6 text-og-control text-og-fg-muted">No diagnostics for this tab.</p>
2265
2982
  )}
2266
2983
  </section>
2267
2984
  </div>
@@ -2308,25 +3025,25 @@ function BrowserDownloadsPanel(props: {
2308
3025
  return (
2309
3026
  <section className="border-b border-og-border p-3" aria-labelledby="browser-downloads-title">
2310
3027
  <div className="flex items-center gap-2">
2311
- <DownloadIcon className="size-3.5 text-og-muted" />
3028
+ <DownloadIcon className="size-3.5 text-og-fg-muted" />
2312
3029
  <h3
2313
3030
  id="browser-downloads-title"
2314
- className="text-[10px] font-semibold uppercase tracking-[0.14em] text-og-subtle"
3031
+ className="text-[10px] font-semibold uppercase tracking-[0.14em] text-og-fg-subtle"
2315
3032
  >
2316
3033
  Downloads
2317
3034
  </h3>
2318
3035
  {props.refreshing ? (
2319
- <LoaderCircleIcon className="ml-auto size-3 animate-spin text-og-muted" />
3036
+ <LoaderCircleIcon className="ml-auto size-3 animate-spin text-og-fg-muted" />
2320
3037
  ) : null}
2321
3038
  </div>
2322
3039
 
2323
3040
  {props.loading ? (
2324
- <div className="flex items-center gap-2 py-5 text-og-control text-og-muted">
3041
+ <div className="flex items-center gap-2 py-5 text-og-control text-og-fg-muted">
2325
3042
  <LoaderCircleIcon className="size-3.5 animate-spin" /> Loading downloads…
2326
3043
  </div>
2327
3044
  ) : props.error ? (
2328
3045
  <div className="py-4">
2329
- <p className="text-og-control text-og-status-error">{props.error.message}</p>
3046
+ <p className="text-og-control text-og-danger">{props.error.message}</p>
2330
3047
  <button
2331
3048
  type="button"
2332
3049
  onClick={props.onRefresh}
@@ -2336,7 +3053,7 @@ function BrowserDownloadsPanel(props: {
2336
3053
  </button>
2337
3054
  </div>
2338
3055
  ) : props.downloads.length === 0 ? (
2339
- <p className="py-4 text-og-control text-og-muted">No files downloaded yet.</p>
3056
+ <p className="py-4 text-og-control text-og-fg-muted">No files downloaded yet.</p>
2340
3057
  ) : (
2341
3058
  <ol className="mt-2 space-y-2">
2342
3059
  {props.downloads.map((download) => {
@@ -2349,9 +3066,9 @@ function BrowserDownloadsPanel(props: {
2349
3066
  className="rounded-og-sm border border-og-border bg-og-bg p-2.5"
2350
3067
  >
2351
3068
  <div className="flex min-w-0 items-start gap-2">
2352
- <span className="mt-0.5 grid size-6 shrink-0 place-items-center rounded bg-og-surface-2 text-og-muted">
3069
+ <span className="mt-0.5 grid size-6 shrink-0 place-items-center rounded bg-og-surface-2 text-og-fg-muted">
2353
3070
  {isSaved ? (
2354
- <FileCheck2Icon className="size-3.5 text-og-status-success" />
3071
+ <FileCheck2Icon className="size-3.5 text-og-status-idle" />
2355
3072
  ) : (
2356
3073
  <DownloadIcon className="size-3.5" />
2357
3074
  )}
@@ -2363,7 +3080,7 @@ function BrowserDownloadsPanel(props: {
2363
3080
  >
2364
3081
  {download.filename}
2365
3082
  </p>
2366
- <p className="mt-0.5 text-[10px] text-og-subtle">
3083
+ <p className="mt-0.5 text-[10px] text-og-fg-subtle">
2367
3084
  {downloadStatusLabel(download)}
2368
3085
  </p>
2369
3086
  </div>
@@ -2385,7 +3102,7 @@ function BrowserDownloadsPanel(props: {
2385
3102
  }}
2386
3103
  disabled={isSaving}
2387
3104
  aria-label={`Workspace path for ${download.filename}`}
2388
- 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"
3105
+ 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"
2389
3106
  />
2390
3107
  <button
2391
3108
  type="button"
@@ -2411,7 +3128,7 @@ function BrowserDownloadsPanel(props: {
2411
3128
  : "Save"}
2412
3129
  </button>
2413
3130
  </div>
2414
- <label className="mt-1.5 flex w-fit items-center gap-1.5 text-[10px] text-og-subtle">
3131
+ <label className="mt-1.5 flex w-fit items-center gap-1.5 text-[10px] text-og-fg-subtle">
2415
3132
  <input
2416
3133
  type="checkbox"
2417
3134
  checked={overwrite[download.id] ?? false}
@@ -2428,7 +3145,7 @@ function BrowserDownloadsPanel(props: {
2428
3145
  Replace an existing workspace file
2429
3146
  </label>
2430
3147
  {errors[download.id] ? (
2431
- <p className="mt-1.5 break-words text-[10px] text-og-status-error">
3148
+ <p className="mt-1.5 break-words text-[10px] text-og-danger">
2432
3149
  {errors[download.id]}
2433
3150
  </p>
2434
3151
  ) : null}
@@ -2471,7 +3188,7 @@ function defaultDownloadDestination(download: BrowserDownload): string {
2471
3188
  function DiagnosticFact(props: { label: string; value: string }) {
2472
3189
  return (
2473
3190
  <>
2474
- <dt className="text-og-subtle">{props.label}</dt>
3191
+ <dt className="text-og-fg-subtle">{props.label}</dt>
2475
3192
  <dd className="min-w-0 truncate text-og-fg" title={props.value}>
2476
3193
  {props.value}
2477
3194
  </dd>
@@ -2479,17 +3196,17 @@ function DiagnosticFact(props: { label: string; value: string }) {
2479
3196
  );
2480
3197
  }
2481
3198
 
2482
- function DiagnosticCount(props: { label: string; value: number }) {
3199
+ function DiagnosticCount(props: { label: string; value: number | undefined }) {
2483
3200
  return (
2484
3201
  <div className="rounded-og-sm border border-og-border bg-og-bg px-2.5 py-2">
2485
- <dt className="text-[10px] text-og-subtle">{props.label}</dt>
3202
+ <dt className="text-[10px] text-og-fg-subtle">{props.label}</dt>
2486
3203
  <dd
2487
3204
  className={cn(
2488
3205
  "mt-0.5 text-og-sm font-semibold",
2489
- props.value ? "text-og-fg" : "text-og-muted",
3206
+ props.value ? "text-og-fg" : "text-og-fg-muted",
2490
3207
  )}
2491
3208
  >
2492
- {props.value}
3209
+ {props.value ?? "Unavailable"}
2493
3210
  </dd>
2494
3211
  </div>
2495
3212
  );
@@ -2520,9 +3237,9 @@ function diagnosticKindLabel(kind: BrowserDiagnosticBatch["entries"][number]["ki
2520
3237
  }
2521
3238
 
2522
3239
  function diagnosticTone(level: BrowserDiagnosticBatch["entries"][number]["level"]): string {
2523
- if (level === "error") return "text-og-status-error";
3240
+ if (level === "error") return "text-og-danger";
2524
3241
  if (level === "warning") return "text-og-status-waiting";
2525
- return "text-og-muted";
3242
+ return "text-og-fg-muted";
2526
3243
  }
2527
3244
 
2528
3245
  function formatDiagnosticTime(value: string): string {
@@ -2536,7 +3253,7 @@ function BrowserNotice(props: { icon: ReactNode; text: string; className?: strin
2536
3253
  return (
2537
3254
  <div
2538
3255
  className={cn(
2539
- "grid h-full place-items-center bg-og-bg text-og-control text-og-muted",
3256
+ "grid h-full place-items-center bg-og-bg text-og-control text-og-fg-muted",
2540
3257
  props.className,
2541
3258
  )}
2542
3259
  >
@@ -2572,7 +3289,7 @@ function loadImage(url: string): Promise<HTMLImageElement> {
2572
3289
 
2573
3290
  function browserPoint(
2574
3291
  canvas: HTMLCanvasElement | null,
2575
- frame: BrowserFrame | null,
3292
+ frame: BrowserViewportFrame | null,
2576
3293
  clientX: number,
2577
3294
  clientY: number,
2578
3295
  ): { x: number; y: number } | null {
@@ -2619,18 +3336,24 @@ function frameMatchesSelectedTarget(
2619
3336
  );
2620
3337
  }
2621
3338
 
2622
- function sameFrameFence(left: BrowserFrame, right: BrowserFrame): boolean {
3339
+ function sameFrameFence(left: BrowserFrameInputFence, right: BrowserFrameInputFence): boolean {
3340
+ return sameBrowserDocument(left, right) && left.frameId === right.frameId;
3341
+ }
3342
+
3343
+ function sameBrowserDocument(left: BrowserFrameInputFence, right: BrowserFrameInputFence): boolean {
2623
3344
  return (
2624
3345
  left.browserSessionId === right.browserSessionId &&
2625
3346
  left.controllerGeneration === right.controllerGeneration &&
2626
3347
  left.targetId === right.targetId &&
2627
3348
  left.targetGeneration === right.targetGeneration &&
2628
- left.documentGeneration === right.documentGeneration &&
2629
- left.frameId === right.frameId
3349
+ left.documentGeneration === right.documentGeneration
2630
3350
  );
2631
3351
  }
2632
3352
 
2633
- function sameOptionalBrowserFrame(left: BrowserFrame | null, right: BrowserFrame | null): boolean {
3353
+ function sameOptionalBrowserFrame(
3354
+ left: BrowserViewportFrame | null,
3355
+ right: BrowserViewportFrame | null,
3356
+ ): boolean {
2634
3357
  return left === null || right === null ? left === right : sameFrameFence(left, right);
2635
3358
  }
2636
3359
 
@@ -2638,6 +3361,23 @@ function isLiveBrowser(session: BrowserSession): boolean {
2638
3361
  return !["ending", "ended", "failed", "lost"].includes(session.lifecycle);
2639
3362
  }
2640
3363
 
3364
+ function attachedChromeGenerationLoss(
3365
+ sessions: readonly BrowserSession[],
3366
+ ): { deviceId: string } | null {
3367
+ const lost = sessions.find(
3368
+ (session) =>
3369
+ session.lifecycle === "lost" &&
3370
+ session.failureCode === "controller_transition_expired" &&
3371
+ session.placement.kind === "attached_device",
3372
+ );
3373
+ return lost?.placement.kind === "attached_device" ? { deviceId: lost.placement.deviceId } : null;
3374
+ }
3375
+
3376
+ function isAttachedChromeGenerationLossError(error: Error | null): boolean {
3377
+ if (!error) return false;
3378
+ return /placement instance changed|controller authority changed/i.test(error.message);
3379
+ }
3380
+
2641
3381
  function countInterventions(
2642
3382
  interventions: readonly InteractionIntervention[],
2643
3383
  ): Map<string, number> {