@opengeni/react 1.0.1 → 1.0.2-canary.37202341988001

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 (632) hide show
  1. package/README.md +660 -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-SHYKQRNX.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-26ZNDDLZ.js +453 -0
  18. package/dist/chunk-26ZNDDLZ.js.map +1 -0
  19. package/dist/chunk-3CTRFMAK.js +359 -0
  20. package/dist/chunk-3CTRFMAK.js.map +1 -0
  21. package/dist/chunk-447JLCBU.js +209 -0
  22. package/dist/chunk-447JLCBU.js.map +1 -0
  23. package/dist/chunk-4RVYOY4O.js +379 -0
  24. package/dist/chunk-4RVYOY4O.js.map +1 -0
  25. package/dist/chunk-5CVXZA3Y.js +292 -0
  26. package/dist/chunk-5CVXZA3Y.js.map +1 -0
  27. package/dist/chunk-5J4FOW6Q.js +108 -0
  28. package/dist/chunk-5J4FOW6Q.js.map +1 -0
  29. package/dist/chunk-742CZ3L3.js +481 -0
  30. package/dist/chunk-742CZ3L3.js.map +1 -0
  31. package/dist/chunk-A2PI6UMY.js +41 -0
  32. package/dist/chunk-A2PI6UMY.js.map +1 -0
  33. package/dist/{chunk-7WCLAEFV.js → chunk-A4UJB2MM.js} +37 -27
  34. package/dist/chunk-A4UJB2MM.js.map +1 -0
  35. package/dist/chunk-B3V6TG77.js +41 -0
  36. package/dist/chunk-B3V6TG77.js.map +1 -0
  37. package/dist/chunk-BRM6FVZA.js +302 -0
  38. package/dist/chunk-BRM6FVZA.js.map +1 -0
  39. package/dist/chunk-C5X5AJXK.js +182 -0
  40. package/dist/chunk-C5X5AJXK.js.map +1 -0
  41. package/dist/{chunk-6KLX2RH5.js → chunk-DO544OSV.js} +1075 -79
  42. package/dist/chunk-DO544OSV.js.map +1 -0
  43. package/dist/{chunk-R3AZOAB5.js → chunk-DYWBUX5Q.js} +29 -123
  44. package/dist/chunk-DYWBUX5Q.js.map +1 -0
  45. package/dist/{chunk-HA46CYQK.js → chunk-EESD3PY3.js} +792 -537
  46. package/dist/chunk-EESD3PY3.js.map +1 -0
  47. package/dist/{chunk-5UQPNLIO.js → chunk-EN3YTHKR.js} +4117 -3896
  48. package/dist/chunk-EN3YTHKR.js.map +1 -0
  49. package/dist/chunk-ERGYJIBB.js +21 -0
  50. package/dist/chunk-ERGYJIBB.js.map +1 -0
  51. package/dist/chunk-FZCP527Y.js +116 -0
  52. package/dist/chunk-FZCP527Y.js.map +1 -0
  53. package/dist/chunk-GIMRELBW.js +14 -0
  54. package/dist/chunk-GIMRELBW.js.map +1 -0
  55. package/dist/chunk-GYVRFZDY.js +2733 -0
  56. package/dist/chunk-GYVRFZDY.js.map +1 -0
  57. package/dist/{chunk-LLOLUHVG.js → chunk-HDJWDK7W.js} +1358 -2746
  58. package/dist/chunk-HDJWDK7W.js.map +1 -0
  59. package/dist/chunk-I7IWRQLN.js +1608 -0
  60. package/dist/chunk-I7IWRQLN.js.map +1 -0
  61. package/dist/chunk-IT4BJUGF.js +13055 -0
  62. package/dist/chunk-IT4BJUGF.js.map +1 -0
  63. package/dist/chunk-IWIDV7SB.js +1 -0
  64. package/dist/chunk-IWIDV7SB.js.map +1 -0
  65. package/dist/chunk-J2QWVSQY.js +312 -0
  66. package/dist/chunk-J2QWVSQY.js.map +1 -0
  67. package/dist/chunk-J6DBS4T3.js +371 -0
  68. package/dist/chunk-J6DBS4T3.js.map +1 -0
  69. package/dist/{chunk-B6WPZSTT.js → chunk-JIQYUGVL.js} +1331 -1163
  70. package/dist/chunk-JIQYUGVL.js.map +1 -0
  71. package/dist/chunk-JT4JW7BC.js +54 -0
  72. package/dist/chunk-JT4JW7BC.js.map +1 -0
  73. package/dist/chunk-K3HQAKQX.js +24 -0
  74. package/dist/chunk-K3HQAKQX.js.map +1 -0
  75. package/dist/{chunk-PEI6RANI.js → chunk-PNY3JKBJ.js} +309 -74
  76. package/dist/chunk-PNY3JKBJ.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-RLUSC2WA.js +299 -0
  80. package/dist/chunk-RLUSC2WA.js.map +1 -0
  81. package/dist/{chunk-IGC6A2ZI.js → chunk-RSI7XYXL.js} +777 -246
  82. package/dist/chunk-RSI7XYXL.js.map +1 -0
  83. package/dist/chunk-SCOTPE2I.js +589 -0
  84. package/dist/chunk-SCOTPE2I.js.map +1 -0
  85. package/dist/{chunk-C5ZPSSS5.js → chunk-TKOTKRWY.js} +5 -4
  86. package/dist/{chunk-C5ZPSSS5.js.map → chunk-TKOTKRWY.js.map} +1 -1
  87. package/dist/chunk-TMYGL6X5.js +742 -0
  88. package/dist/chunk-TMYGL6X5.js.map +1 -0
  89. package/dist/chunk-UGLEYEMP.js +33 -0
  90. package/dist/chunk-UGLEYEMP.js.map +1 -0
  91. package/dist/chunk-UK4TINKT.js +27 -0
  92. package/dist/chunk-UK4TINKT.js.map +1 -0
  93. package/dist/chunk-UTRTG6LH.js +220 -0
  94. package/dist/chunk-UTRTG6LH.js.map +1 -0
  95. package/dist/chunk-WEHMPOXK.js +39 -0
  96. package/dist/chunk-WEHMPOXK.js.map +1 -0
  97. package/dist/chunk-WGXVEJO5.js +319 -0
  98. package/dist/chunk-WGXVEJO5.js.map +1 -0
  99. package/dist/chunk-WRHIZYZF.js +988 -0
  100. package/dist/chunk-WRHIZYZF.js.map +1 -0
  101. package/dist/{chunk-HUIKIDM5.js → chunk-WUISZ3WU.js} +223 -115
  102. package/dist/chunk-WUISZ3WU.js.map +1 -0
  103. package/dist/chunk-XCHWDJ22.js +107 -0
  104. package/dist/chunk-XCHWDJ22.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-YM2DE7BQ.js +3124 -0
  110. package/dist/chunk-YM2DE7BQ.js.map +1 -0
  111. package/dist/{chunk-KZ73RL76.js → chunk-YQEAA6LT.js} +7 -3
  112. package/dist/chunk-YQEAA6LT.js.map +1 -0
  113. package/dist/chunk-YV4L3ILF.js +9 -0
  114. package/dist/chunk-YV4L3ILF.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 +58 -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 +89 -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/session-proxy-scope.d.ts +21 -0
  185. package/dist/components/timeline-anchor.d.ts +24 -0
  186. package/dist/components/timeline-annotation-chrome.d.ts +25 -0
  187. package/dist/components/timeline-annotation-layout.d.ts +42 -0
  188. package/dist/components/timeline-annotation-markers.d.ts +8 -0
  189. package/dist/components/timeline-annotation-reveal-context.d.ts +4 -0
  190. package/dist/components/timeline-annotation-shared.d.ts +31 -0
  191. package/dist/components/timeline-annotations-dialog.d.ts +3 -2
  192. package/dist/components/timeline-annotations.d.ts +13 -3
  193. package/dist/components/timeline-reading-progress.d.ts +11 -0
  194. package/dist/components/timeline-search.d.ts +34 -0
  195. package/dist/components/timeline-settlement.d.ts +16 -0
  196. package/dist/components/tip-follow.d.ts +16 -0
  197. package/dist/components/usage-limit-notice.d.ts +49 -0
  198. package/dist/components/usage-member-list.d.ts +91 -0
  199. package/dist/components/usage-meter.d.ts +73 -0
  200. package/dist/components/use-viewer-menu-dismiss.d.ts +2 -0
  201. package/dist/components/user-message-body.d.ts +10 -1
  202. package/dist/components/workspace-dock.d.ts +6 -2
  203. package/dist/composer.d.ts +5 -4
  204. package/dist/composer.js +15 -9
  205. package/dist/computer-viewer-6SRGWRUX.js +19 -0
  206. package/dist/computer-viewer-6SRGWRUX.js.map +1 -0
  207. package/dist/connect-account-identity.d.ts +8 -0
  208. package/dist/connect-accounts.d.ts +15 -0
  209. package/dist/connect-chooser.d.ts +18 -0
  210. package/dist/connect-panel.d.ts +18 -0
  211. package/dist/connect-setup.d.ts +16 -0
  212. package/dist/connect.d.ts +21 -0
  213. package/dist/connect.js +2129 -0
  214. package/dist/connect.js.map +1 -0
  215. package/dist/connection-catalog.d.ts +38 -0
  216. package/dist/connection-discovery.d.ts +12 -0
  217. package/dist/connection-installed.d.ts +17 -0
  218. package/dist/connection-logo.d.ts +9 -0
  219. package/dist/connection-service-logo.d.ts +8 -0
  220. package/dist/connection-service-presentation.d.ts +13 -0
  221. package/dist/connection-skeleton.d.ts +5 -0
  222. package/dist/connection-type-picker.d.ts +6 -0
  223. package/dist/conversation-timeline.d.ts +5 -0
  224. package/dist/desktop.d.ts +7 -0
  225. package/dist/desktop.js +26 -0
  226. package/dist/desktop.js.map +1 -0
  227. package/dist/device-authorization.d.ts +23 -0
  228. package/dist/diffs.d.ts +8 -0
  229. package/dist/diffs.js +14 -0
  230. package/dist/diffs.js.map +1 -0
  231. package/dist/discovery-cache.d.ts +5 -0
  232. package/dist/editor.d.ts +7 -0
  233. package/dist/editor.js +21 -0
  234. package/dist/editor.js.map +1 -0
  235. package/dist/embedded-session-client.d.ts +23 -0
  236. package/dist/file-node-visibility.d.ts +15 -0
  237. package/dist/{fleet-decision-row-GGCAT4E4.js → fleet-decision-row-YZ3ZKWMM.js} +2 -1
  238. package/dist/fleet-decision-row-YZ3ZKWMM.js.map +1 -0
  239. package/dist/hooks/channel-cache.d.ts +23 -0
  240. package/dist/hooks/latest-question.d.ts +25 -0
  241. package/dist/hooks/shared-machines-list.d.ts +14 -0
  242. package/dist/hooks/use-attached-browsers.d.ts +2 -1
  243. package/dist/hooks/use-browser-frame-stream.d.ts +1 -1
  244. package/dist/hooks/use-browser-session.d.ts +12 -1
  245. package/dist/hooks/use-channels.d.ts +1 -0
  246. package/dist/hooks/use-codex-accounts.d.ts +25 -7
  247. package/dist/hooks/use-composer.d.ts +45 -9
  248. package/dist/hooks/use-computer-frame-stream.d.ts +1 -1
  249. package/dist/hooks/use-computer-input-posture.d.ts +13 -0
  250. package/dist/hooks/use-computer-session.d.ts +2 -0
  251. package/dist/hooks/use-connect.d.ts +22 -0
  252. package/dist/hooks/use-desktop-stream.d.ts +1 -1
  253. package/dist/hooks/use-file-attachments.d.ts +21 -2
  254. package/dist/hooks/use-machine-chip.d.ts +3 -3
  255. package/dist/hooks/use-machines.d.ts +19 -1
  256. package/dist/hooks/use-sandbox-files.d.ts +5 -1
  257. package/dist/hooks/use-session-background-commands.d.ts +14 -0
  258. package/dist/hooks/use-session-events.d.ts +30 -8
  259. package/dist/hooks/use-session-lineage.d.ts +4 -0
  260. package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
  261. package/dist/hooks/use-session.d.ts +6 -0
  262. package/dist/hooks/use-turn-queue.d.ts +12 -0
  263. package/dist/hooks/use-usage.d.ts +40 -0
  264. package/dist/hooks/use-workspace-edit.d.ts +4 -0
  265. package/dist/hooks/use-workspace-sessions.d.ts +27 -5
  266. package/dist/human-input.d.ts +3 -1
  267. package/dist/identity-link-accounts.d.ts +11 -0
  268. package/dist/identity-link-consent.d.ts +18 -0
  269. package/dist/index.d.ts +60 -17
  270. package/dist/index.js +2238 -6201
  271. package/dist/index.js.map +1 -1
  272. package/dist/interaction.d.ts +1 -1
  273. package/dist/interaction.js +35 -184
  274. package/dist/interaction.js.map +1 -1
  275. package/dist/latest-question-HTRIS4KJ.js +117 -0
  276. package/dist/latest-question-HTRIS4KJ.js.map +1 -0
  277. package/dist/lib/error-message.d.ts +7 -0
  278. package/dist/lib/format.d.ts +36 -2
  279. package/dist/lib/host-theme.d.ts +70 -0
  280. package/dist/lib/interaction-errors.d.ts +6 -0
  281. package/dist/lib/menu-styles.d.ts +22 -0
  282. package/dist/lib/pierre-diffs-loader.d.ts +20 -0
  283. package/dist/lib/workbench-peers.d.ts +36 -0
  284. package/dist/lib/xterm-theme.d.ts +7 -5
  285. package/dist/machines.d.ts +4 -2
  286. package/dist/machines.js +14 -4
  287. package/dist/markdown-table-layout-QXSLK4BY.js +106 -0
  288. package/dist/markdown-table-layout-QXSLK4BY.js.map +1 -0
  289. package/dist/mcp-connection-status.d.ts +14 -0
  290. package/dist/model-policy-picker-menu-M54ISBMN.js +14 -0
  291. package/dist/model-policy-picker-menu-M54ISBMN.js.map +1 -0
  292. package/dist/model-policy.d.ts +19 -3
  293. package/dist/model-policy.js +9 -1
  294. package/dist/older-history.d.ts +30 -0
  295. package/dist/platform-activity-row-627BFZ46.js +182 -0
  296. package/dist/platform-activity-row-627BFZ46.js.map +1 -0
  297. package/dist/plugin-details.d.ts +14 -0
  298. package/dist/plugin-discovery.d.ts +25 -0
  299. package/dist/provider.d.ts +13 -1
  300. package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-ZHFI5JBP.js} +131 -44
  301. package/dist/queue-surface-implementation-ZHFI5JBP.js.map +1 -0
  302. package/dist/realtime/realtime-control.d.ts +6 -3
  303. package/dist/realtime.js +133 -74
  304. package/dist/realtime.js.map +1 -1
  305. package/dist/sandbox-files-GORZOUUJ.js +20 -0
  306. package/dist/sandbox-files-GORZOUUJ.js.map +1 -0
  307. package/dist/session-auth-recommendation.d.ts +5 -0
  308. package/dist/session-capability-policy.d.ts +12 -0
  309. package/dist/session-ui.d.ts +30 -2
  310. package/dist/session-ui.js +85 -7
  311. package/dist/session-ui.js.map +1 -1
  312. package/dist/session.d.ts +8 -2
  313. package/dist/session.js +30 -19
  314. package/dist/sites-ui.d.ts +34 -0
  315. package/dist/sites.d.ts +3 -0
  316. package/dist/sites.js +11 -0
  317. package/dist/sites.js.map +1 -0
  318. package/dist/skill-discovery.d.ts +47 -0
  319. package/dist/terminal.d.ts +8 -0
  320. package/dist/terminal.js +31 -0
  321. package/dist/terminal.js.map +1 -0
  322. package/dist/timeline/activity-rail.d.ts +11 -2
  323. package/dist/timeline/allowance-exhausted-row.d.ts +24 -0
  324. package/dist/timeline/compaction-copy.d.ts +10 -0
  325. package/dist/timeline/entrance.d.ts +13 -1
  326. package/dist/timeline/genie-loading.d.ts +34 -0
  327. package/dist/timeline/index.d.ts +6 -2
  328. package/dist/timeline/knowledge-receipt.d.ts +17 -0
  329. package/dist/timeline/platform-activity-row.d.ts +14 -0
  330. package/dist/timeline/presented-image.d.ts +6 -0
  331. package/dist/timeline/projection.d.ts +31 -2
  332. package/dist/timeline/registry.d.ts +2 -0
  333. package/dist/timeline/retained-image.d.ts +18 -0
  334. package/dist/timeline/rolling-activity.d.ts +11 -0
  335. package/dist/timeline/screenshot-lightbox.d.ts +16 -1
  336. package/dist/timeline/shared.d.ts +8 -3
  337. package/dist/timeline/startup-preference.d.ts +3 -0
  338. package/dist/timeline/startup-timings.d.ts +4 -0
  339. package/dist/timeline/tool-display-name.d.ts +2 -1
  340. package/dist/timeline/turn-summary.d.ts +34 -3
  341. package/dist/timeline/types.d.ts +147 -7
  342. package/dist/timeline-annotation-markers-GNDHTGBD.js +165 -0
  343. package/dist/timeline-annotation-markers-GNDHTGBD.js.map +1 -0
  344. package/dist/timeline-annotation-selection-DMLMM2TX.js +241 -0
  345. package/dist/timeline-annotation-selection-DMLMM2TX.js.map +1 -0
  346. package/dist/timeline-annotations-dialog-ELRDBHTS.js +273 -0
  347. package/dist/timeline-annotations-dialog-ELRDBHTS.js.map +1 -0
  348. package/dist/types/machines.d.ts +2 -2
  349. package/dist/usage/allowance-copy.d.ts +25 -0
  350. package/dist/usage/summary.d.ts +35 -0
  351. package/dist/usage.d.ts +13 -0
  352. package/dist/usage.js +1074 -0
  353. package/dist/usage.js.map +1 -0
  354. package/package.json +44 -4
  355. package/src/accounts.tsx +1072 -0
  356. package/src/approvals.ts +8 -1
  357. package/src/artifacts.ts +42 -0
  358. package/src/assets/README.md +8 -0
  359. package/src/assets/gmail.ico +0 -0
  360. package/src/assets/outlook.ico +0 -0
  361. package/src/capability-catalog-row.tsx +94 -0
  362. package/src/capability-logo-fallback.ts +22 -0
  363. package/src/client.ts +22 -13
  364. package/src/commands/registry.ts +18 -15
  365. package/src/commands/types.ts +2 -0
  366. package/src/components/anthropic-mark.tsx +10 -0
  367. package/src/components/approval-surface.tsx +171 -37
  368. package/src/components/artifacts/artifact-chrome.tsx +413 -0
  369. package/src/components/artifacts/artifact-sandbox.tsx +185 -0
  370. package/src/components/artifacts/artifact-surface.tsx +39 -19
  371. package/src/components/artifacts/chat-interactive-block.tsx +260 -0
  372. package/src/components/artifacts/deferred-chat-media.tsx +67 -0
  373. package/src/components/artifacts/document-editor.tsx +141 -16
  374. package/src/components/artifacts/editable-artifact-ui.tsx +4 -2
  375. package/src/components/artifacts/editable-artifact-view.tsx +187 -0
  376. package/src/components/artifacts/editable-artifact-workbench.tsx +5 -2
  377. package/src/components/artifacts/editable-document.tsx +37 -36
  378. package/src/components/artifacts/editable-presentation.tsx +36 -18
  379. package/src/components/artifacts/editable-spreadsheet.tsx +5 -2
  380. package/src/components/artifacts/index.ts +44 -0
  381. package/src/components/artifacts/inline-html-document.ts +24 -0
  382. package/src/components/artifacts/latest-task-runner.ts +49 -0
  383. package/src/components/artifacts/presentation-editor.tsx +323 -67
  384. package/src/components/artifacts/published-html-artifact-frame.tsx +338 -5
  385. package/src/components/artifacts/session-artifact-viewer.tsx +376 -0
  386. package/src/components/artifacts/site-view.tsx +116 -0
  387. package/src/components/artifacts/spreadsheet-canvas.ts +5 -10
  388. package/src/components/artifacts/spreadsheet-general-display.ts +43 -0
  389. package/src/components/artifacts/spreadsheet-grid.tsx +12 -13
  390. package/src/components/artifacts/visualization-assets/base.css +1015 -0
  391. package/src/components/artifacts/visualization-assets/helpers.html +349 -0
  392. package/src/components/artifacts/visualization-assets.gen.ts +3 -0
  393. package/src/components/browser-select-control.tsx +312 -0
  394. package/src/components/browser-viewer.tsx +1054 -314
  395. package/src/components/chat-composer.tsx +131 -85
  396. package/src/components/child-session-link.tsx +27 -0
  397. package/src/components/claude-mark.tsx +10 -0
  398. package/src/components/code-editor.tsx +13 -16
  399. package/src/components/composer-transcription-control.tsx +7 -5
  400. package/src/components/composer.tsx +376 -49
  401. package/src/components/computer-viewer.tsx +1041 -329
  402. package/src/components/copy-button.tsx +13 -5
  403. package/src/components/desktop-viewer.tsx +1 -1
  404. package/src/components/enrollment-consent.tsx +65 -7
  405. package/src/components/enrollment-device-flow.tsx +1 -1
  406. package/src/components/file-browser.tsx +158 -34
  407. package/src/components/fleet-tile.tsx +8 -16
  408. package/src/components/generated-video-player.tsx +4 -2
  409. package/src/components/grok-mark.tsx +33 -0
  410. package/src/components/human-input-form.tsx +455 -114
  411. package/src/components/human-input-surface.tsx +18 -7
  412. package/src/components/interaction-intervention-banner.tsx +2 -2
  413. package/src/components/machine-card.tsx +119 -4
  414. package/src/components/machine-dock-bar.tsx +1 -1
  415. package/src/components/machine-input-display.ts +23 -2
  416. package/src/components/machines/machine-detail.tsx +52 -2
  417. package/src/components/machines/machine-operation-policy.tsx +179 -0
  418. package/src/components/machines-dashboard.tsx +8 -17
  419. package/src/components/markdown-table-layout.ts +132 -0
  420. package/src/components/markdown.tsx +473 -44
  421. package/src/components/message-timeline.tsx +2754 -665
  422. package/src/components/model-mark.tsx +90 -0
  423. package/src/components/model-picker.tsx +15 -4
  424. package/src/components/model-policy-picker-menu.tsx +424 -0
  425. package/src/components/model-policy-picker.tsx +337 -381
  426. package/src/components/open-geni-chat.tsx +338 -0
  427. package/src/components/open-geni-links.tsx +179 -0
  428. package/src/components/openrouter-mark.tsx +13 -0
  429. package/src/components/pierre-diff.tsx +41 -16
  430. package/src/components/pierre-file.tsx +36 -11
  431. package/src/components/preview-loading.tsx +154 -0
  432. package/src/components/queue-item-content.ts +14 -0
  433. package/src/components/queue-surface-implementation.tsx +132 -42
  434. package/src/components/queue-surface-state.tsx +3 -1
  435. package/src/components/sandbox-files.tsx +267 -35
  436. package/src/components/sandbox-terminal.tsx +48 -11
  437. package/src/components/sandbox-workspace.tsx +337 -135
  438. package/src/components/session-capability-frame.tsx +192 -0
  439. package/src/components/session-chrome.tsx +1027 -328
  440. package/src/components/session-commands-panel.tsx +126 -0
  441. package/src/components/session-commands.tsx +18 -0
  442. package/src/components/session-connection-request.tsx +97 -0
  443. package/src/components/session-conversation.tsx +526 -0
  444. package/src/components/session-list.tsx +250 -0
  445. package/src/components/session-mcp-capability-card.tsx +680 -0
  446. package/src/components/session-proxy-scope.tsx +92 -0
  447. package/src/components/session-status.tsx +1 -1
  448. package/src/components/timeline-anchor.tsx +119 -0
  449. package/src/components/timeline-annotation-chrome.tsx +196 -0
  450. package/src/components/timeline-annotation-layout.ts +268 -0
  451. package/src/components/timeline-annotation-markers.tsx +185 -0
  452. package/src/components/timeline-annotation-reveal-context.ts +9 -0
  453. package/src/components/timeline-annotation-selection.tsx +106 -54
  454. package/src/components/timeline-annotation-shared.ts +245 -0
  455. package/src/components/timeline-annotations-dialog.tsx +185 -128
  456. package/src/components/timeline-annotations.tsx +189 -45
  457. package/src/components/timeline-reading-progress.ts +80 -0
  458. package/src/components/timeline-search.tsx +168 -0
  459. package/src/components/timeline-settlement.ts +60 -0
  460. package/src/components/tip-follow.ts +70 -7
  461. package/src/components/tooltip.tsx +38 -5
  462. package/src/components/usage-limit-notice.tsx +179 -0
  463. package/src/components/usage-member-list.tsx +792 -0
  464. package/src/components/usage-meter.tsx +382 -0
  465. package/src/components/use-viewer-menu-dismiss.ts +33 -0
  466. package/src/components/user-message-body.tsx +194 -49
  467. package/src/components/workspace-dock.tsx +141 -175
  468. package/src/composer.ts +6 -1
  469. package/src/connect-account-identity.tsx +34 -0
  470. package/src/connect-accounts.tsx +233 -0
  471. package/src/connect-chooser.tsx +336 -0
  472. package/src/connect-panel.tsx +99 -0
  473. package/src/connect-setup.tsx +307 -0
  474. package/src/connect.ts +60 -0
  475. package/src/connection-assets.d.ts +4 -0
  476. package/src/connection-catalog.tsx +162 -0
  477. package/src/connection-discovery.tsx +235 -0
  478. package/src/connection-installed.tsx +51 -0
  479. package/src/connection-logo.tsx +37 -0
  480. package/src/connection-service-logo.tsx +55 -0
  481. package/src/connection-service-presentation.ts +46 -0
  482. package/src/connection-skeleton.tsx +16 -0
  483. package/src/connection-type-picker.tsx +36 -0
  484. package/src/conversation-timeline.ts +92 -0
  485. package/src/desktop.ts +24 -0
  486. package/src/device-authorization.tsx +98 -0
  487. package/src/diffs.ts +16 -0
  488. package/src/discovery-cache.ts +42 -0
  489. package/src/editor.ts +13 -0
  490. package/src/embedded-session-client.ts +104 -0
  491. package/src/file-node-visibility.ts +66 -0
  492. package/src/hooks/channel-cache.ts +104 -0
  493. package/src/hooks/internal.ts +3 -2
  494. package/src/hooks/latest-question.ts +164 -0
  495. package/src/hooks/shared-machines-list.ts +239 -0
  496. package/src/hooks/use-attached-browsers.ts +5 -1
  497. package/src/hooks/use-available-models.ts +20 -14
  498. package/src/hooks/use-browser-frame-stream.ts +25 -3
  499. package/src/hooks/use-browser-session.ts +271 -71
  500. package/src/hooks/use-browser-sessions.ts +7 -1
  501. package/src/hooks/use-channels.ts +32 -10
  502. package/src/hooks/use-codex-accounts.ts +78 -21
  503. package/src/hooks/use-composer.ts +1034 -320
  504. package/src/hooks/use-computer-frame-stream.ts +39 -16
  505. package/src/hooks/use-computer-input-posture.ts +94 -0
  506. package/src/hooks/use-computer-session.ts +110 -32
  507. package/src/hooks/use-computer-sessions.ts +8 -1
  508. package/src/hooks/use-connect.ts +23 -0
  509. package/src/hooks/use-desktop-stream.ts +19 -28
  510. package/src/hooks/use-file-attachments.ts +118 -10
  511. package/src/hooks/use-goal.ts +36 -9
  512. package/src/hooks/use-human-input.ts +31 -4
  513. package/src/hooks/use-last-started-turn-policy.ts +15 -13
  514. package/src/hooks/use-machine-chip.ts +23 -6
  515. package/src/hooks/use-machines.ts +104 -6
  516. package/src/hooks/use-sandbox-files.ts +253 -150
  517. package/src/hooks/use-sandbox-git.ts +58 -29
  518. package/src/hooks/use-session-background-commands.ts +67 -0
  519. package/src/hooks/use-session-capabilities.ts +21 -2
  520. package/src/hooks/use-session-events.ts +759 -433
  521. package/src/hooks/use-session-lineage.ts +28 -6
  522. package/src/hooks/use-session-mcp-approval-policy.ts +13 -5
  523. package/src/hooks/use-session.ts +75 -16
  524. package/src/hooks/use-slash-commands.ts +6 -11
  525. package/src/hooks/use-turn-queue.ts +113 -13
  526. package/src/hooks/use-usage.ts +100 -0
  527. package/src/hooks/use-video-artifact-playback.ts +1 -1
  528. package/src/hooks/use-voice-input.ts +12 -1
  529. package/src/hooks/use-workspace-capture.ts +3 -2
  530. package/src/hooks/use-workspace-edit.ts +23 -5
  531. package/src/hooks/use-workspace-sessions.ts +99 -11
  532. package/src/human-input.ts +12 -1
  533. package/src/identity-link-accounts.tsx +148 -0
  534. package/src/identity-link-consent.tsx +192 -0
  535. package/src/index.ts +157 -12
  536. package/src/interaction.ts +1 -0
  537. package/src/lib/error-message.ts +37 -0
  538. package/src/lib/format.ts +150 -14
  539. package/src/lib/host-theme.ts +511 -0
  540. package/src/lib/interaction-errors.ts +33 -0
  541. package/src/lib/menu-styles.ts +44 -0
  542. package/src/lib/pierre-diffs-loader.ts +53 -0
  543. package/src/lib/workbench-peers.ts +62 -0
  544. package/src/lib/xterm-theme.ts +23 -21
  545. package/src/machines.ts +9 -1
  546. package/src/mcp-connection-status.ts +35 -0
  547. package/src/model-policy.ts +223 -69
  548. package/src/older-history.ts +87 -0
  549. package/src/plugin-details.tsx +185 -0
  550. package/src/plugin-discovery.tsx +196 -0
  551. package/src/provider.tsx +55 -14
  552. package/src/realtime/dropdown-menu.tsx +19 -12
  553. package/src/realtime/realtime-control.tsx +133 -46
  554. package/src/session-auth-recommendation.ts +48 -0
  555. package/src/session-capability-policy.ts +83 -0
  556. package/src/session-context.ts +4 -3
  557. package/src/session-ui.ts +66 -2
  558. package/src/session.ts +18 -1
  559. package/src/sites-ui.tsx +402 -0
  560. package/src/sites.ts +3 -0
  561. package/src/skill-discovery.tsx +208 -0
  562. package/src/terminal.ts +26 -0
  563. package/src/timeline/activity-rail.tsx +171 -80
  564. package/src/timeline/allowance-exhausted-row.tsx +86 -0
  565. package/src/timeline/compaction-copy.ts +42 -0
  566. package/src/timeline/entrance.tsx +28 -3
  567. package/src/timeline/fleet-decision-projection.ts +2 -1
  568. package/src/timeline/fleet-decision-row.tsx +1 -0
  569. package/src/timeline/genie-loading.tsx +136 -0
  570. package/src/timeline/index.ts +11 -0
  571. package/src/timeline/knowledge-receipt.tsx +100 -0
  572. package/src/timeline/parsers.ts +2 -2
  573. package/src/timeline/platform-activity-row.tsx +216 -0
  574. package/src/timeline/presented-image.ts +49 -0
  575. package/src/timeline/projection.ts +1609 -84
  576. package/src/timeline/registry.ts +15 -3
  577. package/src/timeline/retained-image.ts +97 -0
  578. package/src/timeline/rolling-activity.tsx +92 -0
  579. package/src/timeline/screenshot-lightbox.tsx +115 -17
  580. package/src/timeline/shared.tsx +47 -15
  581. package/src/timeline/startup-preference.ts +33 -0
  582. package/src/timeline/startup-timings.tsx +145 -0
  583. package/src/timeline/tool-display-name.ts +8 -6
  584. package/src/timeline/tool-renderers.tsx +497 -128
  585. package/src/timeline/turn-summary.tsx +218 -44
  586. package/src/timeline/types.ts +192 -7
  587. package/src/types/machines.ts +10 -0
  588. package/src/usage/allowance-copy.ts +64 -0
  589. package/src/usage/summary.ts +93 -0
  590. package/src/usage.ts +50 -0
  591. package/styles/compiled.css +2098 -757
  592. package/styles/connect.css +284 -0
  593. package/styles/connect.d.ts +1 -0
  594. package/styles/effective-tokens.css +1 -0
  595. package/styles/index.css +201 -9
  596. package/styles/responsive.css +27 -1
  597. package/styles/session-capability.css +44 -0
  598. package/styles/tokens.css +135 -65
  599. package/dist/chunk-5UQPNLIO.js.map +0 -1
  600. package/dist/chunk-6KLX2RH5.js.map +0 -1
  601. package/dist/chunk-7WCLAEFV.js.map +0 -1
  602. package/dist/chunk-B6WPZSTT.js.map +0 -1
  603. package/dist/chunk-FK6VG4LL.js +0 -90
  604. package/dist/chunk-FK6VG4LL.js.map +0 -1
  605. package/dist/chunk-HA46CYQK.js.map +0 -1
  606. package/dist/chunk-HUIKIDM5.js.map +0 -1
  607. package/dist/chunk-IGC6A2ZI.js.map +0 -1
  608. package/dist/chunk-KCGXPP5V.js.map +0 -1
  609. package/dist/chunk-KZ73RL76.js.map +0 -1
  610. package/dist/chunk-LAKLF4PA.js +0 -141
  611. package/dist/chunk-LAKLF4PA.js.map +0 -1
  612. package/dist/chunk-LLOLUHVG.js.map +0 -1
  613. package/dist/chunk-MKPIAGVL.js +0 -465
  614. package/dist/chunk-MKPIAGVL.js.map +0 -1
  615. package/dist/chunk-PEI6RANI.js.map +0 -1
  616. package/dist/chunk-R3AZOAB5.js.map +0 -1
  617. package/dist/chunk-WSK7G5LE.js +0 -170
  618. package/dist/chunk-WSK7G5LE.js.map +0 -1
  619. package/dist/chunk-XBZWOM2K.js +0 -218
  620. package/dist/chunk-XBZWOM2K.js.map +0 -1
  621. package/dist/chunk-Y7OEDUJ5.js +0 -8810
  622. package/dist/chunk-Y7OEDUJ5.js.map +0 -1
  623. package/dist/fleet-decision-row-GGCAT4E4.js.map +0 -1
  624. package/dist/hooks/use-packs.d.ts +0 -32
  625. package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
  626. package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
  627. package/dist/timeline-annotation-selection-3L7LHGG2.js +0 -187
  628. package/dist/timeline-annotation-selection-3L7LHGG2.js.map +0 -1
  629. package/dist/timeline-annotations-dialog-U7EVLR75.js +0 -196
  630. package/dist/timeline-annotations-dialog-U7EVLR75.js.map +0 -1
  631. package/src/hooks/use-packs.ts +0 -157
  632. /package/dist/{interactive-workbench-viewers-TOHBXJEE.js.map → browser-viewer-SHYKQRNX.js.map} +0 -0
@@ -8,6 +8,7 @@ import type {
8
8
  InteractionIntervention,
9
9
  InteractionSemanticNode,
10
10
  } from "@opengeni/sdk/interaction";
11
+ import { interactionControlFailureFromError } from "@opengeni/sdk/interaction";
11
12
  import type { DesktopStreamCapability } from "@opengeni/sdk";
12
13
  import {
13
14
  ChevronDownIcon,
@@ -24,6 +25,7 @@ import {
24
25
  } from "lucide-react";
25
26
  import {
26
27
  type ClipboardEvent,
28
+ type CompositionEvent,
27
29
  type KeyboardEvent,
28
30
  type MouseEvent,
29
31
  type PointerEvent,
@@ -31,6 +33,7 @@ import {
31
33
  type WheelEvent,
32
34
  useCallback,
33
35
  useEffect,
36
+ useLayoutEffect,
34
37
  useMemo,
35
38
  useRef,
36
39
  useState,
@@ -40,13 +43,16 @@ import {
40
43
  useComputerFrameStream,
41
44
  } from "../hooks/use-computer-frame-stream";
42
45
  import { useComputerSession } from "../hooks/use-computer-session";
46
+ import { useComputerInputPosture } from "../hooks/use-computer-input-posture";
43
47
  import { useComputerSessions } from "../hooks/use-computer-sessions";
44
48
  import { useInteractionInterventions } from "../hooks/use-interaction-interventions";
45
49
  import { cn } from "../lib/cn";
46
50
  import { copyTextToClipboard } from "../lib/clipboard";
51
+ import { isSourcePlacementChangedError } from "../lib/interaction-errors";
47
52
  import type { EmbeddedComputerInteractionClientOverride } from "../session-context";
48
53
  import { InteractionInterventionBanner } from "./interaction-intervention-banner";
49
54
  import { DesktopViewer } from "./desktop-viewer";
55
+ import { useViewerMenuDismiss } from "./use-viewer-menu-dismiss";
50
56
 
51
57
  export type ComputerViewerNotification = { kind: "error" | "info"; message: string };
52
58
 
@@ -62,6 +68,9 @@ export type ComputerViewerProps = EmbeddedComputerInteractionClientOverride & {
62
68
  /** Host navigation request for one exact ComputerSession. */
63
69
  requestedComputerSessionId?: string | null | undefined;
64
70
  requestedComputerRequestId?: string | number | null | undefined;
71
+ /** Restore and report the Desktop session selected in this task's dock. */
72
+ initialComputerSessionId?: string | null | undefined;
73
+ onComputerSessionIdChange?: ((computerSessionId: string | null) => void) | undefined;
65
74
  };
66
75
 
67
76
  type ComputerSelection = { sessionId: string; pinned: boolean } | null;
@@ -78,6 +87,8 @@ export function ComputerViewer({
78
87
  renderEmpty,
79
88
  requestedComputerSessionId,
80
89
  requestedComputerRequestId,
90
+ initialComputerSessionId,
91
+ onComputerSessionIdChange,
81
92
  ...override
82
93
  }: ComputerViewerProps) {
83
94
  const registry = useComputerSessions({ ...override, sessionId, enabled });
@@ -87,11 +98,26 @@ export function ComputerViewer({
87
98
  () => registry.relevantSessions.filter((session) => isLiveComputer(session)),
88
99
  [registry.relevantSessions],
89
100
  );
101
+ const connectedPlacementLoss = useMemo(
102
+ () =>
103
+ registry.relevantSessions.find(
104
+ (session) =>
105
+ session.lifecycle === "lost" &&
106
+ session.failureCode === "source_placement_changed" &&
107
+ session.placement.kind === "connected_machine",
108
+ ) ?? null,
109
+ [registry.relevantSessions],
110
+ );
90
111
  const recentRelevantFailure = useMemo(
91
112
  () =>
92
113
  liveRelevant.length === 0
93
- ? (registry.relevantSessions.find((session) =>
94
- ["failed", "lost"].includes(session.lifecycle),
114
+ ? (registry.relevantSessions.find(
115
+ (session) =>
116
+ ["failed", "lost"].includes(session.lifecycle) &&
117
+ !(
118
+ session.failureCode === "source_placement_changed" &&
119
+ session.placement.kind === "connected_machine"
120
+ ),
95
121
  ) ?? null)
96
122
  : null,
97
123
  [liveRelevant.length, registry.relevantSessions],
@@ -105,17 +131,46 @@ export function ComputerViewer({
105
131
  liveRelevant.length > 0 ? liveRelevant : recentRelevantFailure ? [recentRelevantFailure] : [],
106
132
  [liveRelevant, recentRelevantFailure],
107
133
  );
108
- const [selection, setSelection] = useState<ComputerSelection>(null);
134
+ const [selection, setSelection] = useState<ComputerSelection>(() =>
135
+ initialComputerSessionId ? { sessionId: initialComputerSessionId, pinned: true } : null,
136
+ );
137
+ const selectComputerSession = useCallback(
138
+ (next: ComputerSelection) => {
139
+ setSelection(next);
140
+ onComputerSessionIdChange?.(next?.sessionId ?? null);
141
+ },
142
+ [onComputerSessionIdChange],
143
+ );
109
144
  const interventions = useInteractionInterventions({
110
145
  ...override,
111
146
  enabled,
112
147
  resourceKind: "computer_session",
113
148
  });
114
149
  const [creating, setCreating] = useState(false);
150
+ const [createError, setCreateError] = useState<Error | null>(null);
115
151
  const [showControls, setShowControls] = useState(false);
116
152
  const previousSessionIdRef = useRef(sessionId);
117
153
  const handledRequestRef = useRef<string | null>(null);
118
154
  const seenInterventionIdsRef = useRef(new Set<string>());
155
+ const createInFlightRef = useRef(false);
156
+ const autoCreateSessionRef = useRef<string | null>(null);
157
+ const emptyCatalogConfirmationRef = useRef<string | null>(null);
158
+ const endedGenerationLossRef = useRef(new Set<string>());
159
+ const recreatedPlacementLossRef = useRef(new Set<string>());
160
+ const [suppressGenericCreate, setSuppressGenericCreate] = useState(false);
161
+ const endStaleComputer = registry.end;
162
+ useEffect(() => {
163
+ if (!enabled) return;
164
+ for (const session of registry.sessions) {
165
+ if (!isAttachedChromeGenerationLoss(session)) continue;
166
+ setSuppressGenericCreate(true);
167
+ if (endedGenerationLossRef.current.has(session.id)) continue;
168
+ endedGenerationLossRef.current.add(session.id);
169
+ void endStaleComputer(session.id).catch(() => {
170
+ endedGenerationLossRef.current.delete(session.id);
171
+ });
172
+ }
173
+ }, [enabled, endStaleComputer, registry.sessions]);
119
174
 
120
175
  const notifyError = useCallback(
121
176
  (cause: unknown, fallback: string) => {
@@ -131,10 +186,18 @@ export function ComputerViewer({
131
186
  if (previousSessionIdRef.current === sessionId) return;
132
187
  previousSessionIdRef.current = sessionId;
133
188
  handledRequestRef.current = null;
134
- setSelection(null);
135
- }, [sessionId]);
189
+ autoCreateSessionRef.current = null;
190
+ emptyCatalogConfirmationRef.current = null;
191
+ recreatedPlacementLossRef.current.clear();
192
+ setSuppressGenericCreate(false);
193
+ setCreateError(null);
194
+ setSelection(
195
+ initialComputerSessionId ? { sessionId: initialComputerSessionId, pinned: true } : null,
196
+ );
197
+ }, [initialComputerSessionId, sessionId]);
136
198
 
137
199
  useEffect(() => {
200
+ if (!enabled || registry.loading) return;
138
201
  const requested = requestedComputerSessionId
139
202
  ? (liveSessions.find((session) => session.id === requestedComputerSessionId) ?? null)
140
203
  : null;
@@ -143,7 +206,7 @@ export function ComputerViewer({
143
206
  : null;
144
207
  if (requested && requestKey && handledRequestRef.current !== requestKey) {
145
208
  handledRequestRef.current = requestKey;
146
- setSelection({ sessionId: requested.id, pinned: true });
209
+ selectComputerSession({ sessionId: requested.id, pinned: true });
147
210
  return;
148
211
  }
149
212
 
@@ -154,15 +217,24 @@ export function ComputerViewer({
154
217
  if (selection?.pinned && selectedStillLive) return;
155
218
  const preferred = relevant[0] ?? null;
156
219
  if (!preferred) {
157
- if (selection) setSelection(null);
220
+ if (selection) selectComputerSession(null);
158
221
  return;
159
222
  }
160
223
  if (!selectedStillLive || !selection?.pinned) {
161
224
  if (selection?.sessionId !== preferred.id || selection.pinned) {
162
- setSelection({ sessionId: preferred.id, pinned: false });
225
+ selectComputerSession({ sessionId: preferred.id, pinned: false });
163
226
  }
164
227
  }
165
- }, [liveSessions, relevant, requestedComputerRequestId, requestedComputerSessionId, selection]);
228
+ }, [
229
+ liveSessions,
230
+ enabled,
231
+ registry.loading,
232
+ relevant,
233
+ requestedComputerRequestId,
234
+ requestedComputerSessionId,
235
+ selectComputerSession,
236
+ selection,
237
+ ]);
166
238
 
167
239
  const selectedRegistrySession = useMemo(
168
240
  () => liveSessions.find((session) => session.id === selection?.sessionId) ?? null,
@@ -185,6 +257,13 @@ export function ComputerViewer({
185
257
  computerSessionId: selection?.sessionId ?? null,
186
258
  enabled: enabled && selection !== null && controllerReady,
187
259
  });
260
+ useEffect(() => {
261
+ setShowControls(computer.selectedTarget?.kind === "app");
262
+ }, [sessionId, selection?.sessionId, computer.selectedTarget?.id, computer.selectedTarget?.kind]);
263
+ useEffect(() => {
264
+ if (!isSourcePlacementChangedError(computer.error, "computer_session")) return;
265
+ void refreshRegistry();
266
+ }, [computer.error, refreshRegistry]);
188
267
  const act = computer.act;
189
268
  const actFromFrame = computer.actFromFrame;
190
269
  const refreshComputer = computer.refresh;
@@ -192,7 +271,12 @@ export function ComputerViewer({
192
271
  ...override,
193
272
  computerSessionId: selection?.sessionId ?? null,
194
273
  targetId: computer.selectedTarget?.id ?? null,
195
- enabled: enabled && selection !== null && controllerReady && computer.selectedTarget !== null,
274
+ enabled:
275
+ enabled &&
276
+ selection !== null &&
277
+ controllerReady &&
278
+ computer.selectedTarget !== null &&
279
+ isComputerFrameTarget(computer.selectedTarget),
196
280
  stream: {
197
281
  format: "jpeg",
198
282
  quality: 78,
@@ -205,18 +289,69 @@ export function ComputerViewer({
205
289
  const displayedFrame =
206
290
  frames.frame &&
207
291
  frames.frame.computerSessionId === selection?.sessionId &&
208
- frames.frame.targetId === computer.selectedTarget?.id
292
+ frames.frame.targetId === computer.selectedTarget?.id &&
293
+ frames.frame.targetGeneration === computer.selectedTarget?.targetGeneration
209
294
  ? frames.frame
210
295
  : null;
211
296
  const rfbStream =
212
297
  frames.attachment?.stream.kind === "direct_rfb" ? frames.attachment.stream : null;
298
+ const appTarget = computer.selectedTarget?.kind === "app";
299
+ const appInputPosture = useComputerInputPosture({
300
+ ...override,
301
+ computerSessionId: selection?.sessionId ?? null,
302
+ controllerGeneration: computer.session?.controller?.controllerGeneration ?? null,
303
+ enabled: enabled && controllerReady && appTarget,
304
+ });
305
+ const inputPostureRef = useRef({
306
+ sessionId: selection?.sessionId ?? null,
307
+ controllerGeneration: computer.session?.controller?.controllerGeneration ?? null,
308
+ denied: false,
309
+ });
310
+ const postureSessionId = selection?.sessionId ?? null;
311
+ const postureController = computer.session?.controller?.controllerGeneration ?? null;
312
+ if (
313
+ inputPostureRef.current.sessionId !== postureSessionId ||
314
+ inputPostureRef.current.controllerGeneration !== postureController
315
+ ) {
316
+ inputPostureRef.current = {
317
+ sessionId: postureSessionId,
318
+ controllerGeneration: postureController,
319
+ denied: false,
320
+ };
321
+ }
322
+ if (
323
+ frames.attachment?.computerSessionId === postureSessionId &&
324
+ frames.attachment.controllerGeneration === postureController
325
+ ) {
326
+ inputPostureRef.current.denied = frames.attachment.inputAllowed === false;
327
+ }
328
+ // Keep a known denial while a fresh attachment is pending or a semantic-only
329
+ // target has no stream. Only a fresh posture for this controller clears it.
330
+ const inputDenied = appTarget
331
+ ? appInputPosture.inputAllowed !== true
332
+ : inputPostureRef.current.denied;
333
+ const attachmentInputAllowed = frames.attachment !== null && !inputDenied;
334
+ const inputPosturePending = appTarget
335
+ ? appInputPosture.pending
336
+ : inputDenied && (frames.state === "attaching" || frames.state === "reconnecting");
337
+ const machineLocked = selectedRegistrySession?.failureCode === "machine_locked";
338
+ const controlUnavailable = computer.controlError !== null;
339
+ const connectionState = controlUnavailable ? "error" : frames.state;
340
+ // RFB has one combined input switch; partial native input stays view-only.
341
+ const rfbInputEnabled =
342
+ rfbStream?.inputAllowed === true &&
343
+ attachmentInputAllowed &&
344
+ !machineLocked &&
345
+ !controlUnavailable &&
346
+ computer.session?.capabilities?.pointerInput === true &&
347
+ computer.session?.capabilities?.keyboardInput === true;
213
348
  const rfbCapability = useMemo<DesktopStreamCapability | null>(() => {
214
349
  if (!rfbStream || !frames.attachment) return null;
215
350
  const bounds = computer.selectedTarget?.bounds;
216
351
  return {
217
352
  transport: "vnc-ws",
218
353
  client: "novnc",
219
- mode: "interactive",
354
+ mode: rfbInputEnabled ? "interactive" : "read-only",
220
355
  url: rfbStream.url,
221
356
  token: null,
222
357
  expiresAt: frames.attachment.expiresAt,
@@ -228,8 +363,7 @@ export function ComputerViewer({
228
363
  sharedSessionIds: [],
229
364
  reason: null,
230
365
  };
231
- }, [computer.selectedTarget?.bounds, frames.attachment, rfbStream]);
232
- const machineLocked = selectedRegistrySession?.failureCode === "machine_locked";
366
+ }, [computer.selectedTarget?.bounds, frames.attachment, rfbInputEnabled, rfbStream]);
233
367
  const currentIds = useMemo(() => new Set(relevant.map((session) => session.id)), [relevant]);
234
368
 
235
369
  useEffect(() => {
@@ -257,54 +391,150 @@ export function ComputerViewer({
257
391
  outcome === "completed" ? "Agent notified. Continuing work." : "Request cancelled.",
258
392
  });
259
393
  })
260
- .catch((cause) => notifyError(cause, "Could not update the computer request."));
394
+ .catch((cause) => notifyError(cause, "Could not update the desktop request."));
261
395
  },
262
396
  [interventions, notifyError, onNotify],
263
397
  );
264
398
 
265
399
  const createComputer = useCallback(() => {
266
- if (creating) return;
400
+ if (createInFlightRef.current) return;
401
+ createInFlightRef.current = true;
402
+ setCreateError(null);
267
403
  setCreating(true);
268
- void createSession({ sessionId, name: "Computer" })
404
+ void createSession({ sessionId, name: "Desktop" })
269
405
  .then((response) => {
270
- setSelection({ sessionId: response.session.id, pinned: false });
406
+ selectComputerSession({ sessionId: response.session.id, pinned: false });
271
407
  })
272
- .catch((cause) => notifyError(cause, "Could not open a computer."))
273
- .finally(() => setCreating(false));
274
- }, [createSession, creating, notifyError, sessionId]);
408
+ .catch((cause) => {
409
+ const error = cause instanceof Error ? cause : new Error("Could not open a desktop.");
410
+ setCreateError(error);
411
+ notifyError(error, "Could not open a desktop.");
412
+ })
413
+ .finally(() => {
414
+ createInFlightRef.current = false;
415
+ setCreating(false);
416
+ });
417
+ }, [createSession, notifyError, selectComputerSession, sessionId]);
418
+
419
+ useEffect(() => {
420
+ if (
421
+ !enabled ||
422
+ registry.loading ||
423
+ registry.refreshing ||
424
+ liveRelevant.length > 0 ||
425
+ !connectedPlacementLoss ||
426
+ recreatedPlacementLossRef.current.has(connectedPlacementLoss.id)
427
+ ) {
428
+ return;
429
+ }
430
+ recreatedPlacementLossRef.current.add(connectedPlacementLoss.id);
431
+ autoCreateSessionRef.current = sessionId;
432
+ emptyCatalogConfirmationRef.current = null;
433
+ createComputer();
434
+ }, [
435
+ connectedPlacementLoss,
436
+ createComputer,
437
+ enabled,
438
+ liveRelevant.length,
439
+ registry.loading,
440
+ registry.refreshing,
441
+ sessionId,
442
+ ]);
443
+
444
+ useEffect(() => {
445
+ // Once this task's Desktop has been observed, a later transient empty
446
+ // registry snapshot must not create a second one. This can happen while the
447
+ // lazy surface is disabled/re-enabled or while an invalidation refresh
448
+ // briefly races the association projection. The existing Desktop remains
449
+ // the task's authority; explicit New desktop is still available.
450
+ if (relevant.length > 0) {
451
+ autoCreateSessionRef.current = sessionId;
452
+ emptyCatalogConfirmationRef.current = null;
453
+ }
454
+ }, [relevant.length, sessionId]);
455
+
456
+ useEffect(() => {
457
+ if (
458
+ !enabled ||
459
+ registry.loading ||
460
+ registry.refreshing ||
461
+ registry.error ||
462
+ relevant.length > 0 ||
463
+ suppressGenericCreate ||
464
+ autoCreateSessionRef.current === sessionId
465
+ ) {
466
+ return;
467
+ }
468
+ // A lazy surface can momentarily publish an empty settled projection while
469
+ // its initial catalog and a workspace invalidation cross. Confirm the empty
470
+ // result with one authoritative refresh before creating anything. A real
471
+ // empty catalog creates on the following render; an existing association
472
+ // appears and cancels this path.
473
+ if (emptyCatalogConfirmationRef.current !== sessionId) {
474
+ emptyCatalogConfirmationRef.current = sessionId;
475
+ void refreshRegistry();
476
+ return;
477
+ }
478
+ autoCreateSessionRef.current = sessionId;
479
+ createComputer();
480
+ }, [
481
+ createComputer,
482
+ enabled,
483
+ refreshRegistry,
484
+ registry.error,
485
+ registry.loading,
486
+ registry.refreshing,
487
+ relevant.length,
488
+ sessionId,
489
+ suppressGenericCreate,
490
+ ]);
275
491
 
276
492
  const perform = useCallback(
277
493
  async (action: ComputerAction, frame: ComputerFrame | null): Promise<void> => {
494
+ if (computer.controlError) throw computer.controlError;
495
+ if (inputDenied) throw new Error("Desktop is view only. Refresh to check desktop controls.");
496
+ let receipt;
278
497
  if (action.type === "pointer") {
279
- if (!frame) throw new Error("Computer view is not ready for pointer input.");
280
- await actFromFrame(action, frame);
498
+ if (!frame) throw new Error("Desktop view is not ready for pointer input.");
499
+ receipt = await actFromFrame(action, frame);
281
500
  } else {
282
- await act(action);
501
+ receipt = await act(action);
502
+ }
503
+ if (receipt.state !== "completed") {
504
+ throw new Error(receipt.error?.message ?? "Desktop input did not complete.");
283
505
  }
284
506
  },
285
- [act, actFromFrame],
507
+ [act, actFromFrame, computer.controlError, inputDenied],
286
508
  );
287
509
 
510
+ const reconnect = () => {
511
+ void refreshComputer();
512
+ if (appTarget) appInputPosture.refresh();
513
+ else frames.reconnect();
514
+ };
515
+
288
516
  const copyFromRfb = useCallback(
289
517
  (event: ClipboardEvent<HTMLDivElement>) => {
290
- if (!rfbStream || computer.session?.capabilities?.clipboard !== true) return;
518
+ if (!rfbInputEnabled || !rfbStream || computer.session?.capabilities?.clipboard !== true)
519
+ return;
291
520
  event.preventDefault();
292
521
  event.stopPropagation();
293
522
  void perform({ type: "clipboard", operation: "copy" }, null)
294
523
  .then(() => computer.readClipboard())
295
524
  .then(async (clipboard) => {
296
525
  if (clipboard.text && !(await copyTextToClipboard(clipboard.text))) {
297
- throw new Error("Computer text could not be copied to the local clipboard");
526
+ throw new Error("Desktop text could not be copied to the local clipboard");
298
527
  }
299
528
  })
300
- .catch((cause) => notifyError(cause, "Could not copy from the computer."));
529
+ .catch((cause) => notifyError(cause, "Could not copy from the desktop."));
301
530
  },
302
- [computer, notifyError, perform, rfbStream],
531
+ [computer, notifyError, perform, rfbInputEnabled, rfbStream],
303
532
  );
304
533
 
305
534
  const pasteIntoRfb = useCallback(
306
535
  (text: string): boolean => {
307
- if (!rfbStream || computer.session?.capabilities?.clipboard !== true) return false;
536
+ if (!rfbInputEnabled || !rfbStream || computer.session?.capabilities?.clipboard !== true)
537
+ return false;
308
538
  // Keep paste on the canonical ComputerSession action path. RFB
309
539
  // ClientCutText synchronization varies by server and can acknowledge a
310
540
  // local paste without ever updating the remote graphical seat. These two
@@ -316,49 +546,76 @@ export function ComputerViewer({
316
546
  await perform({ type: "clipboard", operation: "write", text }, null);
317
547
  assertExactComputerClipboard(await computer.readClipboard(), text);
318
548
  await perform({ type: "clipboard", operation: "paste" }, null);
319
- })().catch((cause) => notifyError(cause, "Could not paste into the computer."));
549
+ })().catch((cause) => notifyError(cause, "Could not paste into the desktop."));
320
550
  return true;
321
551
  },
322
- [computer, notifyError, perform, rfbStream],
552
+ [computer, notifyError, perform, rfbInputEnabled, rfbStream],
323
553
  );
324
554
 
555
+ const hideGenericCreate = suppressGenericCreate && liveRelevant.length === 0;
556
+ const generationLossSelected =
557
+ selectedRegistrySession !== null && isAttachedChromeGenerationLoss(selectedRegistrySession);
558
+ const generationLossFrames = isAttachedChromeGenerationLossError(frames.error);
559
+
325
560
  if (!enabled) return null;
326
561
  if (registry.loading && liveSessions.length === 0) {
327
562
  return (
328
563
  <ComputerNotice
329
564
  icon={<LoaderCircleIcon className="size-4 animate-spin" />}
330
- text="Finding workspace computers…"
565
+ text="Finding workspace desktops…"
331
566
  {...(className ? { className } : {})}
332
567
  />
333
568
  );
334
569
  }
335
570
  if (liveSessions.length === 0) {
571
+ if (hideGenericCreate) {
572
+ return (
573
+ <div className={cn("grid h-full place-items-center bg-og-bg p-6", className)}>
574
+ <div className="max-w-sm text-center">
575
+ <CircleAlertIcon className="mx-auto size-5 text-og-danger" />
576
+ <p className="mt-3 text-og-menu font-medium text-og-fg">
577
+ Chrome reconnected—open a fresh browser/desktop.
578
+ </p>
579
+ <p className="mt-1 text-og-control leading-5 text-og-fg-muted">
580
+ This Chrome profile is live again, but the previous desktop cannot move to the new
581
+ connection. Use Browser → New browser → Connected Chrome.
582
+ </p>
583
+ </div>
584
+ </div>
585
+ );
586
+ }
336
587
  if (renderEmpty) return renderEmpty(createComputer, creating);
588
+ const error = createError ?? registry.error;
337
589
  return (
338
590
  <div className={cn("grid h-full place-items-center bg-og-bg p-6", className)}>
339
591
  <div className="max-w-sm text-center">
340
- <span className="mx-auto grid size-10 place-items-center rounded-og-lg border border-og-border bg-og-surface-1 text-og-muted">
341
- <MonitorIcon className="size-5" />
342
- </span>
343
- <p className="mt-3 text-og-menu font-medium text-og-fg">No computer open</p>
344
- <p className="mt-1 text-og-control leading-5 text-og-muted">
345
- Open this agent&apos;s desktop, or switch to any computer another workspace agent uses.
346
- </p>
347
- <button
348
- type="button"
349
- onClick={createComputer}
350
- disabled={creating}
351
- className="mt-4 inline-flex h-8 items-center gap-1.5 rounded-og-sm bg-og-accent-deep px-3 text-og-control font-medium text-og-accent-fg transition hover:brightness-110 disabled:opacity-50"
352
- >
353
- {creating ? (
354
- <LoaderCircleIcon className="size-3.5 animate-spin" />
592
+ <span className="mx-auto grid size-10 place-items-center rounded-og-lg border border-og-border bg-og-surface-1 text-og-fg-muted">
593
+ {error ? (
594
+ <CircleAlertIcon className="size-5 text-og-danger" />
355
595
  ) : (
356
- <PlusIcon className="size-3.5" />
596
+ <LoaderCircleIcon className="size-5 animate-spin" />
357
597
  )}
358
- Open computer
359
- </button>
360
- {registry.error ? (
361
- <p className="mt-3 text-og-control text-og-status-error">{registry.error.message}</p>
598
+ </span>
599
+ <p className="mt-3 text-og-menu font-medium text-og-fg">
600
+ {error ? "Desktop didn’t open" : "Opening desktop…"}
601
+ </p>
602
+ <p className="mt-1 text-og-control leading-5 text-og-fg-muted">
603
+ {error?.message ?? "Preparing this agent’s desktop. It will appear here when ready."}
604
+ </p>
605
+ {error ? (
606
+ <button
607
+ type="button"
608
+ onClick={createError ? createComputer : () => void refreshRegistry()}
609
+ disabled={creating || registry.refreshing}
610
+ className="mt-4 inline-flex h-8 items-center gap-1.5 rounded-og-sm border border-og-primary-border bg-og-primary text-og-primary-fg px-3 text-og-control font-medium transition hover:bg-og-primary-hover disabled:opacity-50"
611
+ >
612
+ {creating || registry.refreshing ? (
613
+ <LoaderCircleIcon className="size-3.5 animate-spin" />
614
+ ) : (
615
+ <RotateCcwIcon className="size-3.5" />
616
+ )}
617
+ Try again
618
+ </button>
362
619
  ) : null}
363
620
  </div>
364
621
  </div>
@@ -366,22 +623,46 @@ export function ComputerViewer({
366
623
  }
367
624
 
368
625
  return (
369
- <div className={cn("flex h-full min-h-0 flex-col overflow-hidden bg-og-bg", className)}>
626
+ <div
627
+ className={cn(
628
+ "@container/computer-viewer relative flex h-full min-h-0 min-w-0 flex-col overflow-hidden bg-og-bg",
629
+ className,
630
+ )}
631
+ >
370
632
  <ComputerToolbar
371
633
  sessions={liveSessions}
372
634
  relevantSessionIds={currentIds}
373
635
  selectedSessionId={selection?.sessionId ?? null}
374
636
  creating={creating}
375
- refreshing={registry.refreshing}
637
+ refreshing={registry.refreshing || inputPosturePending}
376
638
  interventionCounts={interventionCounts}
639
+ targets={controllerReady ? computer.targets : []}
640
+ selectedTargetId={computer.selectedTarget?.id ?? null}
641
+ targetsLoading={controllerReady && computer.loading}
642
+ onSelectTarget={(targetId) => {
643
+ void computer
644
+ .selectTarget(targetId)
645
+ .catch((cause) => notifyError(cause, "Could not switch desktop views."));
646
+ }}
377
647
  onSelect={(computerSessionId) =>
378
- setSelection({ sessionId: computerSessionId, pinned: true })
648
+ selectComputerSession({ sessionId: computerSessionId, pinned: true })
379
649
  }
380
650
  onFollow={() => {
381
- if (relevant[0]) setSelection({ sessionId: relevant[0].id, pinned: false });
651
+ if (relevant[0]) selectComputerSession({ sessionId: relevant[0].id, pinned: false });
652
+ }}
653
+ onCreate={hideGenericCreate ? undefined : createComputer}
654
+ onRefresh={() => {
655
+ if (appTarget) {
656
+ appInputPosture.refresh();
657
+ } else if (
658
+ (connectionState === "error" || inputDenied) &&
659
+ !generationLossSelected &&
660
+ !generationLossFrames
661
+ ) {
662
+ frames.reconnect();
663
+ }
664
+ void Promise.all([refreshRegistry(), refreshComputer()]);
382
665
  }}
383
- onCreate={createComputer}
384
- onRefresh={() => void Promise.all([refreshRegistry(), refreshComputer()])}
385
666
  />
386
667
  <InteractionInterventionBanner
387
668
  interventions={selectedInterventions}
@@ -390,7 +671,7 @@ export function ComputerViewer({
390
671
  onOpen={(intervention) =>
391
672
  void computer
392
673
  .selectTarget(intervention.targetId)
393
- .catch((cause) => notifyError(cause, "Could not open the requested computer view."))
674
+ .catch((cause) => notifyError(cause, "Could not open the requested desktop view."))
394
675
  }
395
676
  onResolve={resolveIntervention}
396
677
  />
@@ -398,6 +679,8 @@ export function ComputerViewer({
398
679
  <ComputerUnselectedPanel
399
680
  peerCount={liveSessions.length}
400
681
  creating={creating}
682
+ error={createError}
683
+ generationLoss={suppressGenericCreate}
401
684
  onCreate={createComputer}
402
685
  />
403
686
  ) : !controllerReady ? (
@@ -409,53 +692,84 @@ export function ComputerViewer({
409
692
  />
410
693
  ) : (
411
694
  <>
412
- <ComputerTargetRail
413
- targets={computer.targets}
414
- selectedTargetId={computer.selectedTarget?.id ?? null}
415
- loading={computer.loading}
416
- onSelect={(targetId) =>
417
- void computer
418
- .selectTarget(targetId)
419
- .catch((cause) => notifyError(cause, "Could not switch computer views."))
420
- }
421
- />
422
- <div className="flex min-h-0 flex-1">
695
+ <div className="flex min-h-0 flex-1 flex-col @xl/computer-viewer:flex-row">
423
696
  {rfbStream ? (
424
697
  <div className="relative min-h-0 flex-1 bg-black" onCopyCapture={copyFromRfb}>
425
698
  <DesktopViewer
426
699
  capability={rfbCapability}
427
- interactive
700
+ interactive={rfbInputEnabled}
428
701
  showControlToggle={false}
429
702
  webSocketProtocols={rfbStream.protocols}
430
703
  onPasteText={pasteIntoRfb}
431
704
  targetPlatform={computer.session?.platform ?? null}
432
705
  className="h-full"
433
706
  />
707
+ {!rfbInputEnabled ? (
708
+ <div className="pointer-events-none absolute bottom-3 left-1/2 -translate-x-1/2 rounded-full border border-white/10 bg-black/70 px-3 py-1 text-[11px] text-white/80 backdrop-blur">
709
+ {controlUnavailable ? (
710
+ <>
711
+ Desktop controls unavailable{" "}
712
+ <button
713
+ type="button"
714
+ onClick={reconnect}
715
+ className="pointer-events-auto ml-2 font-medium underline"
716
+ >
717
+ Reconnect
718
+ </button>
719
+ </>
720
+ ) : (
721
+ "View only · direct input unavailable"
722
+ )}
723
+ </div>
724
+ ) : null}
434
725
  </div>
435
726
  ) : (
436
727
  <ComputerViewport
728
+ key={JSON.stringify([
729
+ selection?.sessionId,
730
+ computer.selectedTarget?.id,
731
+ computer.selectedTarget?.targetGeneration,
732
+ frames.attachment?.controllerGeneration,
733
+ ])}
437
734
  frame={machineLocked ? null : displayedFrame}
438
735
  observation={computer.observation}
439
736
  target={computer.selectedTarget}
440
737
  machineLocked={machineLocked}
441
- connectionState={frames.state}
442
- connectionError={frames.error ?? computer.error}
738
+ controlUnavailable={controlUnavailable}
739
+ inputAllowed={!inputDenied}
740
+ connectionState={connectionState}
741
+ connectionError={
742
+ computer.controlError ?? appInputPosture.error ?? frames.error ?? computer.error
743
+ }
443
744
  mutating={computer.mutating}
444
745
  backgroundActions={computer.session?.capabilities?.backgroundActions === true}
445
- clipboardEnabled={computer.session?.capabilities?.clipboard === true}
746
+ backgroundInput={computer.session?.capabilities?.backgroundInput === true}
747
+ clipboardEnabled={
748
+ attachmentInputAllowed && computer.session?.capabilities?.clipboard === true
749
+ }
750
+ pointerInput={
751
+ attachmentInputAllowed && computer.session?.capabilities?.pointerInput === true
752
+ }
753
+ keyboardInput={
754
+ attachmentInputAllowed && computer.session?.capabilities?.keyboardInput === true
755
+ }
446
756
  onAction={perform}
447
757
  onReadClipboard={computer.readClipboard}
448
- onReconnect={frames.reconnect}
449
- onError={(cause) => notifyError(cause, "Computer input failed.")}
758
+ onReconnect={
759
+ generationLossSelected || generationLossFrames ? () => undefined : reconnect
760
+ }
761
+ onError={(cause) => notifyError(cause, "Desktop input failed.")}
450
762
  />
451
763
  )}
452
764
  {showControls ? (
453
765
  <ComputerSemanticPanel
454
766
  observation={computer.observation}
455
- mutating={computer.mutating}
767
+ mutating={computer.mutating || controlUnavailable || inputDenied}
768
+ controlUnavailable={controlUnavailable}
769
+ inputAllowed={!inputDenied}
456
770
  onAction={(action) =>
457
771
  void perform(action, null).catch((cause) =>
458
- notifyError(cause, "Computer action failed."),
772
+ notifyError(cause, "Desktop action failed."),
459
773
  )
460
774
  }
461
775
  />
@@ -464,10 +778,12 @@ export function ComputerViewer({
464
778
  <ComputerStatusBar
465
779
  session={computer.session}
466
780
  target={computer.selectedTarget}
467
- connectionState={frames.state}
781
+ connectionState={connectionState}
782
+ controlUnavailable={controlUnavailable}
783
+ inputAllowed={!inputDenied}
468
784
  refreshing={registry.refreshing}
469
785
  showControls={showControls}
470
- controlCount={semanticNodes(computer.observation).length}
786
+ controlCount={semanticControls(computer.observation).length}
471
787
  onToggleControls={() => setShowControls((current) => !current)}
472
788
  />
473
789
  </>
@@ -479,33 +795,50 @@ export function ComputerViewer({
479
795
  function ComputerUnselectedPanel(props: {
480
796
  peerCount: number;
481
797
  creating: boolean;
798
+ error: Error | null;
799
+ generationLoss: boolean;
482
800
  onCreate: () => void;
483
801
  }) {
484
802
  return (
485
803
  <div className="grid min-h-0 flex-1 place-items-center bg-og-bg p-6 text-center">
486
804
  <div className="max-w-sm">
487
- <span className="mx-auto grid size-10 place-items-center rounded-og-md border border-og-border bg-og-surface-1 text-og-muted">
488
- <MonitorIcon className="size-4.5" />
489
- </span>
490
- <p className="mt-3 text-og-menu font-medium text-og-fg">No computer for this agent</p>
491
- <p className="mt-1 text-og-control leading-5 text-og-muted">
492
- {props.peerCount === 1
493
- ? "One workspace computer is available from the computer menu."
494
- : `${props.peerCount} workspace computers are available from the computer menu.`}
495
- </p>
496
- <button
497
- type="button"
498
- disabled={props.creating}
499
- onClick={props.onCreate}
500
- 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"
501
- >
502
- {props.creating ? (
503
- <LoaderCircleIcon className="size-3.5 animate-spin" />
805
+ <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">
806
+ {props.error || props.generationLoss ? (
807
+ <CircleAlertIcon className="size-4.5 text-og-danger" />
504
808
  ) : (
505
- <PlusIcon className="size-3.5" />
809
+ <LoaderCircleIcon className="size-4.5 animate-spin" />
506
810
  )}
507
- {props.creating ? "Opening…" : "New computer"}
508
- </button>
811
+ </span>
812
+ <p className="mt-3 text-og-menu font-medium text-og-fg">
813
+ {props.generationLoss
814
+ ? "Chrome reconnected—open a fresh browser/desktop."
815
+ : props.error
816
+ ? "Desktop didn’t open"
817
+ : "Opening desktop…"}
818
+ </p>
819
+ <p className="mt-1 text-og-control leading-5 text-og-fg-muted">
820
+ {props.generationLoss
821
+ ? "This Chrome profile is live again, but the previous desktop cannot move to the new connection. Use Browser → New browser → Connected Chrome."
822
+ : (props.error?.message ??
823
+ (props.peerCount === 1
824
+ ? "Preparing this agent’s desktop. One peer desktop remains available from the menu."
825
+ : `Preparing this agent’s desktop. ${props.peerCount} peer desktops remain available from the menu.`))}
826
+ </p>
827
+ {props.error && !props.generationLoss ? (
828
+ <button
829
+ type="button"
830
+ disabled={props.creating}
831
+ onClick={props.onCreate}
832
+ 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"
833
+ >
834
+ {props.creating ? (
835
+ <LoaderCircleIcon className="size-3.5 animate-spin" />
836
+ ) : (
837
+ <RotateCcwIcon className="size-3.5" />
838
+ )}
839
+ Try again
840
+ </button>
841
+ ) : null}
509
842
  </div>
510
843
  </div>
511
844
  );
@@ -518,79 +851,116 @@ function ComputerToolbar(props: {
518
851
  creating: boolean;
519
852
  refreshing: boolean;
520
853
  interventionCounts: Map<string, number>;
854
+ targets: ComputerTarget[];
855
+ selectedTargetId: string | null;
856
+ targetsLoading: boolean;
857
+ onSelectTarget: (id: string) => void;
521
858
  onSelect: (id: string) => void;
522
859
  onFollow: () => void;
523
- onCreate: () => void;
860
+ onCreate?: (() => void) | undefined;
524
861
  onRefresh: () => void;
525
862
  }) {
526
- const detailsRef = useRef<HTMLDetailsElement | null>(null);
863
+ const detailsRef = useViewerMenuDismiss();
527
864
  const selected = props.sessions.find((session) => session.id === props.selectedSessionId);
528
865
  const current = props.sessions.filter((session) => props.relevantSessionIds.has(session.id));
529
866
  const others = props.sessions.filter((session) => !props.relevantSessionIds.has(session.id));
530
- const choose = (id: string) => {
531
- props.onSelect(id);
867
+ const closeMenu = (restoreFocus: boolean) => {
532
868
  detailsRef.current?.removeAttribute("open");
869
+ if (restoreFocus) detailsRef.current?.querySelector("summary")?.focus();
870
+ };
871
+ const choose = (id: string, restoreFocus: boolean) => {
872
+ props.onSelect(id);
873
+ closeMenu(restoreFocus);
533
874
  };
534
875
  return (
535
- <div className="flex h-10 shrink-0 items-center gap-1.5 border-b border-og-border bg-og-surface-0 px-2">
536
- <details ref={detailsRef} className="relative min-w-0">
537
- <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">
538
- <MonitorIcon className="size-3.5 shrink-0 text-og-muted" />
539
- <span className="truncate font-medium">{selected?.name ?? "Computer"}</span>
876
+ <div className="flex h-10 shrink-0 items-center gap-1.5 border-b border-og-border bg-og-surface-1 px-2">
877
+ <details ref={detailsRef} className="min-w-0">
878
+ <summary className="flex h-7 max-w-32 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 @sm/computer-viewer:max-w-52 [&::-webkit-details-marker]:hidden">
879
+ <MonitorIcon className="size-3.5 shrink-0 text-og-fg-muted" />
880
+ <span className="truncate font-medium">
881
+ {selected?.name === "Computer" ? "Desktop" : (selected?.name ?? "Desktop")}
882
+ </span>
540
883
  {(props.interventionCounts.get(selected?.id ?? "") ?? 0) > 0 ? (
541
884
  <span className="size-1.5 shrink-0 rounded-full bg-og-status-waiting" />
542
885
  ) : null}
543
- <ChevronDownIcon className="size-3 shrink-0 text-og-subtle" />
886
+ <ChevronDownIcon className="size-3 shrink-0 text-og-fg-subtle" />
544
887
  </summary>
545
- <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">
546
- <ComputerSessionGroup
547
- label="Current agent"
548
- sessions={current}
549
- selectedId={props.selectedSessionId}
550
- interventionCounts={props.interventionCounts}
551
- onSelect={choose}
552
- />
553
- <ComputerSessionGroup
554
- label="Other agents"
555
- sessions={others}
556
- selectedId={props.selectedSessionId}
557
- interventionCounts={props.interventionCounts}
558
- onSelect={choose}
559
- />
560
- <div className="mt-1 flex gap-1 border-t border-og-border pt-1">
888
+ <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">
889
+ <div className="min-h-0 max-h-96 overflow-y-auto overscroll-contain">
890
+ <ComputerSessionGroup
891
+ label="Current agent"
892
+ sessions={current}
893
+ selectedId={props.selectedSessionId}
894
+ interventionCounts={props.interventionCounts}
895
+ onSelect={choose}
896
+ />
897
+ <ComputerSessionGroup
898
+ label="Other agents"
899
+ sessions={others}
900
+ selectedId={props.selectedSessionId}
901
+ interventionCounts={props.interventionCounts}
902
+ onSelect={choose}
903
+ />
904
+ </div>
905
+ <div className="mt-1 flex shrink-0 gap-1 border-t border-og-border pt-1">
561
906
  {current.length > 0 ? (
562
- <MenuButton onClick={props.onFollow}>Follow agent</MenuButton>
907
+ <MenuButton
908
+ onClick={(event) => {
909
+ props.onFollow();
910
+ closeMenu(event.detail === 0);
911
+ }}
912
+ >
913
+ Follow agent
914
+ </MenuButton>
915
+ ) : null}
916
+ {props.onCreate ? (
917
+ <MenuButton
918
+ onClick={(event) => {
919
+ props.onCreate?.();
920
+ closeMenu(event.detail === 0);
921
+ }}
922
+ disabled={props.creating}
923
+ >
924
+ <PlusIcon className="size-3.5" /> New desktop
925
+ </MenuButton>
563
926
  ) : null}
564
- <MenuButton onClick={props.onCreate} disabled={props.creating}>
565
- <PlusIcon className="size-3.5" /> New computer
566
- </MenuButton>
567
927
  </div>
568
928
  </div>
569
929
  </details>
570
- <span className="min-w-0 flex-1 truncate text-og-xs text-og-subtle">
930
+ <span className="min-w-0 flex-1 truncate text-og-xs text-og-fg-subtle">
571
931
  {selected ? `${platformLabel(selected)} · ${placementLabel(selected)}` : ""}
572
932
  </span>
933
+ <ComputerTargetPicker
934
+ targets={props.targets}
935
+ selectedTargetId={props.selectedTargetId}
936
+ loading={props.targetsLoading}
937
+ onSelect={props.onSelectTarget}
938
+ />
573
939
  <button
574
940
  type="button"
575
941
  onClick={props.onRefresh}
576
- className="grid size-7 place-items-center rounded-og-sm text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg"
577
- aria-label="Refresh computers"
942
+ disabled={props.refreshing}
943
+ aria-busy={props.refreshing}
944
+ className="grid size-7 place-items-center rounded-og-sm text-og-fg-muted transition hover:bg-og-surface-2 hover:text-og-fg"
945
+ aria-label="Refresh desktops"
578
946
  >
579
947
  <RefreshCwIcon className={cn("size-3.5", props.refreshing && "animate-spin")} />
580
948
  </button>
581
- <button
582
- type="button"
583
- onClick={props.onCreate}
584
- disabled={props.creating}
585
- 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"
586
- aria-label="Open a new computer"
587
- >
588
- {props.creating ? (
589
- <LoaderCircleIcon className="size-3.5 animate-spin" />
590
- ) : (
591
- <PlusIcon className="size-3.5" />
592
- )}
593
- </button>
949
+ {props.onCreate ? (
950
+ <button
951
+ type="button"
952
+ onClick={props.onCreate}
953
+ disabled={props.creating}
954
+ className="hidden size-7 place-items-center rounded-og-sm text-og-fg-muted transition hover:bg-og-surface-2 hover:text-og-fg disabled:opacity-40 @sm/computer-viewer:grid"
955
+ aria-label="Open a new desktop"
956
+ >
957
+ {props.creating ? (
958
+ <LoaderCircleIcon className="size-3.5 animate-spin" />
959
+ ) : (
960
+ <PlusIcon className="size-3.5" />
961
+ )}
962
+ </button>
963
+ ) : null}
594
964
  </div>
595
965
  );
596
966
  }
@@ -600,12 +970,12 @@ function ComputerSessionGroup(props: {
600
970
  sessions: ComputerSession[];
601
971
  selectedId: string | null;
602
972
  interventionCounts: Map<string, number>;
603
- onSelect: (id: string) => void;
973
+ onSelect: (id: string, restoreFocus: boolean) => void;
604
974
  }) {
605
975
  if (props.sessions.length === 0) return null;
606
976
  return (
607
977
  <div className="py-1">
608
- <p className="px-2 pb-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-subtle">
978
+ <p className="px-2 pb-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-fg-subtle">
609
979
  {props.label}
610
980
  </p>
611
981
  {props.sessions.map((session) => {
@@ -614,7 +984,8 @@ function ComputerSessionGroup(props: {
614
984
  <button
615
985
  key={session.id}
616
986
  type="button"
617
- onClick={() => props.onSelect(session.id)}
987
+ onClick={(event) => props.onSelect(session.id, event.detail === 0)}
988
+ aria-pressed={session.id === props.selectedId}
618
989
  className={cn(
619
990
  "flex w-full items-center gap-2 rounded-og-sm px-2 py-1.5 text-left transition hover:bg-og-surface-2",
620
991
  session.id === props.selectedId && "bg-og-surface-2",
@@ -628,7 +999,7 @@ function ComputerSessionGroup(props: {
628
999
  />
629
1000
  <span className="min-w-0 flex-1">
630
1001
  <span className="block truncate text-og-control text-og-fg">{session.name}</span>
631
- <span className="block truncate text-og-xs text-og-subtle">
1002
+ <span className="block truncate text-og-xs text-og-fg-subtle">
632
1003
  {platformLabel(session)} · {placementLabel(session)}
633
1004
  </span>
634
1005
  </span>
@@ -644,65 +1015,119 @@ function ComputerSessionGroup(props: {
644
1015
  );
645
1016
  }
646
1017
 
647
- function MenuButton(props: { children: ReactNode; onClick: () => void; disabled?: boolean }) {
1018
+ function MenuButton(props: {
1019
+ children: ReactNode;
1020
+ onClick: (event: MouseEvent<HTMLButtonElement>) => void;
1021
+ disabled?: boolean;
1022
+ }) {
648
1023
  return (
649
1024
  <button
650
1025
  type="button"
651
1026
  onClick={props.onClick}
652
1027
  disabled={props.disabled}
653
- 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"
1028
+ 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"
654
1029
  >
655
1030
  {props.children}
656
1031
  </button>
657
1032
  );
658
1033
  }
659
1034
 
660
- function ComputerTargetRail(props: {
1035
+ function ComputerTargetPicker(props: {
661
1036
  targets: ComputerTarget[];
662
1037
  selectedTargetId: string | null;
663
1038
  loading: boolean;
664
1039
  onSelect: (id: string) => void;
665
1040
  }) {
666
- const visualTargets = props.targets.filter(isRenderableComputerView);
1041
+ const screens = props.targets.filter((target) => target.kind === "screen");
1042
+ const advancedTargets = props.targets.filter(
1043
+ (target) =>
1044
+ target.kind === "app" || (target.kind === "window" && isRenderableComputerView(target)),
1045
+ );
1046
+ const advancedRef = useViewerMenuDismiss(advancedTargets.length > 0);
1047
+ const selected = props.targets.find((target) => target.id === props.selectedTargetId);
1048
+ const choose = (id: string, restoreFocus = false) => {
1049
+ props.onSelect(id);
1050
+ if (advancedRef.current?.open) {
1051
+ advancedRef.current.removeAttribute("open");
1052
+ if (restoreFocus) advancedRef.current.querySelector("summary")?.focus();
1053
+ }
1054
+ };
667
1055
  return (
668
- <div
669
- className="flex h-10 shrink-0 items-center gap-1 overflow-x-auto border-b border-og-border bg-og-surface-0 px-2"
670
- aria-label="Computer views"
671
- >
672
- {visualTargets.map((target) => (
1056
+ <>
1057
+ {screens.length > 1 ? (
1058
+ <select
1059
+ aria-label="Screen"
1060
+ value={selected?.kind === "screen" ? selected.id : ""}
1061
+ onChange={(event) => choose(event.currentTarget.value)}
1062
+ className="h-7 min-w-0 max-w-28 rounded-og-sm border border-og-border bg-og-surface-1 px-1.5 text-og-control text-og-fg outline-hidden focus:border-og-accent @sm/computer-viewer:max-w-40"
1063
+ >
1064
+ <option value="" disabled hidden>
1065
+ Screen
1066
+ </option>
1067
+ {screens.map((screen, index) => (
1068
+ <option key={screen.id} value={screen.id}>
1069
+ {screen.title || `Screen ${index + 1}`}
1070
+ </option>
1071
+ ))}
1072
+ </select>
1073
+ ) : screens[0] && selected?.kind !== "screen" ? (
673
1074
  <button
674
- key={target.id}
675
1075
  type="button"
676
- onClick={() => props.onSelect(target.id)}
677
- className={cn(
678
- "flex h-7 max-w-56 shrink-0 items-center gap-1.5 rounded-og-sm border px-2 text-og-control transition",
679
- target.id === props.selectedTargetId
680
- ? "border-og-border-strong bg-og-surface-2 text-og-fg"
681
- : "border-transparent text-og-muted hover:bg-og-surface-1 hover:text-og-fg",
682
- )}
683
- aria-pressed={target.id === props.selectedTargetId}
1076
+ onClick={() => choose(screens[0]!.id)}
1077
+ className="flex h-7 shrink-0 items-center gap-1.5 rounded-og-sm px-2 text-og-control text-og-fg-muted transition hover:bg-og-surface-2 hover:text-og-fg"
684
1078
  >
685
- {target.kind === "screen" ? (
686
- <MonitorIcon className="size-3.5 shrink-0" />
687
- ) : (
688
- <PanelsTopLeftIcon className="size-3.5 shrink-0" />
689
- )}
690
- <span className="truncate">{target.title || target.applicationId || target.kind}</span>
691
- {target.focused ? (
692
- <span
693
- className="size-1.5 shrink-0 rounded-full bg-og-status-running"
694
- aria-label="Focused"
695
- />
696
- ) : null}
1079
+ <MonitorIcon className="size-3.5" /> Full desktop
697
1080
  </button>
698
- ))}
699
- {props.loading ? (
700
- <LoaderCircleIcon className="ml-1 size-3.5 animate-spin text-og-muted" />
701
1081
  ) : null}
702
- {!props.loading && visualTargets.length === 0 ? (
703
- <span className="text-og-xs text-og-subtle">Waiting for apps and screens…</span>
1082
+ {advancedTargets.length > 0 ? (
1083
+ <details ref={advancedRef} className="shrink-0">
1084
+ <summary
1085
+ aria-label="Advanced desktop views"
1086
+ title="Advanced desktop views"
1087
+ className="flex h-7 cursor-pointer list-none items-center gap-1.5 rounded-og-sm px-2 text-og-control text-og-fg-muted transition hover:bg-og-surface-2 hover:text-og-fg [&::-webkit-details-marker]:hidden"
1088
+ >
1089
+ <PanelsTopLeftIcon className="size-3.5" />
1090
+ <span className="hidden @sm/computer-viewer:inline">Advanced</span>
1091
+ <ChevronDownIcon className="size-3" />
1092
+ </summary>
1093
+ <div className="absolute right-2 top-10 z-30 flex max-h-[calc(100%-3rem)] w-64 max-w-[calc(100%-1rem)] flex-col overflow-hidden rounded-og-md border border-og-border bg-og-surface-1 p-1 shadow-xl">
1094
+ <p className="shrink-0 px-2 py-1 text-og-xs text-og-fg-subtle">Apps and windows</p>
1095
+ <div className="min-h-0 max-h-80 overflow-y-auto overscroll-contain">
1096
+ {advancedTargets.map((target) => (
1097
+ <button
1098
+ key={target.id}
1099
+ type="button"
1100
+ onClick={(event) => choose(target.id, event.detail === 0)}
1101
+ aria-pressed={target.id === props.selectedTargetId}
1102
+ className={cn(
1103
+ "flex w-full min-w-0 items-center gap-2 rounded-og-sm px-2 py-1.5 text-left transition hover:bg-og-surface-2",
1104
+ target.id === props.selectedTargetId && "bg-og-surface-2",
1105
+ )}
1106
+ >
1107
+ <span className="min-w-0 flex-1">
1108
+ <span className="block truncate text-og-control text-og-fg">
1109
+ {target.title || target.applicationId || target.kind}
1110
+ </span>
1111
+ <span className="block text-og-xs text-og-fg-subtle">
1112
+ {target.kind === "app" ? "App controls" : "Window view"}
1113
+ </span>
1114
+ </span>
1115
+ {target.focused ? (
1116
+ <span
1117
+ className="size-1.5 shrink-0 rounded-full bg-og-status-running"
1118
+ aria-label="Focused"
1119
+ />
1120
+ ) : null}
1121
+ </button>
1122
+ ))}
1123
+ </div>
1124
+ </div>
1125
+ </details>
704
1126
  ) : null}
705
- </div>
1127
+ {props.loading ? (
1128
+ <LoaderCircleIcon className="size-3.5 shrink-0 animate-spin text-og-fg-muted" />
1129
+ ) : null}
1130
+ </>
706
1131
  );
707
1132
  }
708
1133
 
@@ -715,30 +1140,41 @@ function isRenderableComputerView(target: ComputerTarget): boolean {
715
1140
  return target.bounds.width >= 160 && target.bounds.height >= 90;
716
1141
  }
717
1142
 
1143
+ function isComputerFrameTarget(target: ComputerTarget): boolean {
1144
+ return target.kind === "window" || target.kind === "screen";
1145
+ }
1146
+
718
1147
  function ComputerLifecyclePanel(props: {
719
1148
  session: ComputerSession;
720
1149
  creating: boolean;
721
1150
  onRefresh: () => Promise<void>;
722
1151
  onRetry: () => void;
723
1152
  }) {
1153
+ const generationLoss = isAttachedChromeGenerationLoss(props.session);
724
1154
  const failed = ["failed", "lost", "repair_required"].includes(props.session.lifecycle);
725
1155
  const retryCreatesSession = ["failed", "lost"].includes(props.session.lifecycle);
726
1156
  return (
727
1157
  <div className="grid min-h-0 flex-1 place-items-center bg-og-bg p-6">
728
1158
  <div className="max-w-sm text-center">
729
1159
  {failed ? (
730
- <CircleAlertIcon className="mx-auto size-5 text-og-status-error" />
1160
+ <CircleAlertIcon className="mx-auto size-5 text-og-danger" />
731
1161
  ) : (
732
- <LoaderCircleIcon className="mx-auto size-5 animate-spin text-og-muted" />
1162
+ <LoaderCircleIcon className="mx-auto size-5 animate-spin text-og-fg-muted" />
733
1163
  )}
734
1164
  <p className="mt-3 text-og-menu font-medium text-og-fg">
735
- {failed ? "Computer needs attention" : lifecycleLabel(props.session.lifecycle)}
1165
+ {generationLoss
1166
+ ? "Chrome reconnected—open a fresh browser/desktop."
1167
+ : failed
1168
+ ? "Desktop needs attention"
1169
+ : lifecycleLabel(props.session.lifecycle)}
736
1170
  </p>
737
- <p className="mt-1 text-og-control leading-5 text-og-muted">
738
- {computerFailureMessage(props.session) ??
739
- "The computer is being prepared on its placement. It will appear here when ready."}
1171
+ <p className="mt-1 text-og-control leading-5 text-og-fg-muted">
1172
+ {generationLoss
1173
+ ? "This Chrome profile is live again, but the previous desktop cannot move to the new connection. Use Browser → New browser → Connected Chrome."
1174
+ : (computerFailureMessage(props.session) ??
1175
+ "The desktop is being prepared on its placement. It will appear here when ready.")}
740
1176
  </p>
741
- {failed ? (
1177
+ {failed && !generationLoss ? (
742
1178
  <button
743
1179
  type="button"
744
1180
  onClick={() => (retryCreatesSession ? props.onRetry() : void props.onRefresh())}
@@ -763,11 +1199,16 @@ function ComputerViewport(props: {
763
1199
  observation: ComputerObservation | null;
764
1200
  target: ComputerTarget | null;
765
1201
  machineLocked: boolean;
1202
+ controlUnavailable: boolean;
1203
+ inputAllowed: boolean;
766
1204
  connectionState: string;
767
1205
  connectionError: Error | null;
768
1206
  mutating: boolean;
769
1207
  backgroundActions: boolean;
1208
+ backgroundInput: boolean;
770
1209
  clipboardEnabled: boolean;
1210
+ pointerInput: boolean;
1211
+ keyboardInput: boolean;
771
1212
  onAction: (action: ComputerAction, frame: ComputerFrame | null) => Promise<void>;
772
1213
  onReadClipboard: () => Promise<ComputerClipboard>;
773
1214
  onReconnect: () => void;
@@ -775,6 +1216,7 @@ function ComputerViewport(props: {
775
1216
  }) {
776
1217
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
777
1218
  const inputRef = useRef<HTMLTextAreaElement | null>(null);
1219
+ const composingRef = useRef(false);
778
1220
  const pointerStartRef = useRef<PointerStart | null>(null);
779
1221
  const clickTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
780
1222
  const lastClickRef = useRef<{
@@ -799,14 +1241,58 @@ function ComputerViewport(props: {
799
1241
  const readClipboardRef = useRef(props.onReadClipboard);
800
1242
  const errorRef = useRef(props.onError);
801
1243
  const actionTailRef = useRef<Promise<void>>(Promise.resolve());
1244
+ const actionQueueEpochRef = useRef(0);
802
1245
  const queuedFrameRef = useRef<ComputerFrame | null>(null);
1246
+ const currentFrameRef = useRef<ComputerFrame | null>(props.frame);
1247
+ const paintedFrameRef = useRef<ComputerFrame | null>(null);
1248
+ const [paintedFrame, setPaintedFrame] = useState<ComputerFrame | null>(null);
803
1249
  const decodingFrameRef = useRef(false);
804
1250
  const mountedRef = useRef(true);
805
1251
  actionRef.current = props.onAction;
806
1252
  readClipboardRef.current = props.onReadClipboard;
807
1253
  errorRef.current = props.onError;
1254
+ const streamFailed = props.connectionState === "error";
808
1255
  const rawInputEnabled =
809
- !props.backgroundActions || props.target?.kind === "screen" || props.target?.focused === true;
1256
+ props.target?.kind !== "app" &&
1257
+ !streamFailed &&
1258
+ !props.controlUnavailable &&
1259
+ !props.machineLocked &&
1260
+ (props.backgroundInput ||
1261
+ !props.backgroundActions ||
1262
+ props.target?.kind === "screen" ||
1263
+ props.target?.focused === true);
1264
+
1265
+ const clearBufferedInput = useCallback(() => {
1266
+ if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
1267
+ if (wheelRef.current?.timer) clearTimeout(wheelRef.current.timer);
1268
+ if (pendingTextRef.current?.timer) clearTimeout(pendingTextRef.current.timer);
1269
+ clickTimerRef.current = null;
1270
+ wheelRef.current = null;
1271
+ pendingTextRef.current = null;
1272
+ pointerStartRef.current = null;
1273
+ lastClickRef.current = null;
1274
+ composingRef.current = false;
1275
+ if (inputRef.current) inputRef.current.value = "";
1276
+ }, []);
1277
+
1278
+ const pointerInputEnabled = rawInputEnabled && props.pointerInput;
1279
+ const keyboardInputEnabled = rawInputEnabled && props.keyboardInput;
1280
+
1281
+ useLayoutEffect(() => {
1282
+ const canvas = canvasRef.current;
1283
+ const viewport = canvas?.parentElement;
1284
+ if (!canvas || !viewport) return;
1285
+ const fit = () => fitComputerCanvas(canvas);
1286
+ fit();
1287
+ if (typeof ResizeObserver === "undefined") {
1288
+ window.addEventListener("resize", fit);
1289
+ return () => window.removeEventListener("resize", fit);
1290
+ }
1291
+ // The canvas is absolute, so fitting it cannot resize the observed dock.
1292
+ const observer = new ResizeObserver(fit);
1293
+ observer.observe(viewport);
1294
+ return () => observer.disconnect();
1295
+ }, []);
810
1296
 
811
1297
  const paintQueuedFrames = useCallback(() => {
812
1298
  if (decodingFrameRef.current) return;
@@ -824,8 +1310,15 @@ function ComputerViewport(props: {
824
1310
  const bitmap = await createImageBitmap(blob);
825
1311
  try {
826
1312
  const canvas = canvasRef.current;
827
- if (mountedRef.current && canvas) {
1313
+ if (
1314
+ mountedRef.current &&
1315
+ canvas &&
1316
+ currentFrameRef.current &&
1317
+ sameComputerTarget(frame, currentFrameRef.current)
1318
+ ) {
828
1319
  paintCanvas(canvas, bitmap, frame.width, frame.height);
1320
+ paintedFrameRef.current = frame;
1321
+ setPaintedFrame(frame);
829
1322
  }
830
1323
  } finally {
831
1324
  bitmap.close();
@@ -835,11 +1328,23 @@ function ComputerViewport(props: {
835
1328
  objectUrl = URL.createObjectURL(blob);
836
1329
  const image = await loadImage(objectUrl);
837
1330
  const canvas = canvasRef.current;
838
- if (mountedRef.current && canvas) {
1331
+ if (
1332
+ mountedRef.current &&
1333
+ canvas &&
1334
+ currentFrameRef.current &&
1335
+ sameComputerTarget(frame, currentFrameRef.current)
1336
+ ) {
839
1337
  paintCanvas(canvas, image, frame.width, frame.height);
1338
+ paintedFrameRef.current = frame;
1339
+ setPaintedFrame(frame);
840
1340
  }
841
1341
  } catch (cause) {
842
- if (mountedRef.current) errorRef.current(cause);
1342
+ if (
1343
+ mountedRef.current &&
1344
+ currentFrameRef.current &&
1345
+ sameComputerTarget(frame, currentFrameRef.current)
1346
+ )
1347
+ errorRef.current(cause);
843
1348
  } finally {
844
1349
  if (objectUrl) URL.revokeObjectURL(objectUrl);
845
1350
  }
@@ -851,40 +1356,64 @@ function ComputerViewport(props: {
851
1356
  })();
852
1357
  }, []);
853
1358
 
854
- useEffect(() => {
1359
+ useLayoutEffect(() => {
855
1360
  mountedRef.current = true;
856
1361
  return () => {
857
1362
  mountedRef.current = false;
1363
+ actionQueueEpochRef.current += 1;
858
1364
  queuedFrameRef.current = null;
1365
+ clearBufferedInput();
859
1366
  };
860
- }, []);
1367
+ }, [clearBufferedInput]);
861
1368
 
862
- useEffect(() => {
863
- if (!props.frame) return;
1369
+ useLayoutEffect(() => {
1370
+ if (currentFrameRef.current && !props.frame) {
1371
+ actionQueueEpochRef.current += 1;
1372
+ clearBufferedInput();
1373
+ }
1374
+ currentFrameRef.current = props.frame;
1375
+ if (!props.frame) {
1376
+ queuedFrameRef.current = null;
1377
+ paintedFrameRef.current = null;
1378
+ setPaintedFrame(null);
1379
+ return;
1380
+ }
864
1381
  queuedFrameRef.current = props.frame;
865
1382
  paintQueuedFrames();
866
- }, [paintQueuedFrames, props.frame]);
1383
+ }, [clearBufferedInput, paintQueuedFrames, props.frame]);
867
1384
 
868
- useEffect(
869
- () => () => {
870
- if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
871
- if (wheelRef.current?.timer) clearTimeout(wheelRef.current.timer);
872
- if (pendingTextRef.current?.timer) clearTimeout(pendingTextRef.current.timer);
873
- },
874
- [],
875
- );
1385
+ useLayoutEffect(() => {
1386
+ if (!rawInputEnabled || !props.pointerInput || !props.keyboardInput) {
1387
+ actionQueueEpochRef.current += 1;
1388
+ clearBufferedInput();
1389
+ }
1390
+ }, [clearBufferedInput, props.keyboardInput, props.pointerInput, rawInputEnabled]);
876
1391
 
877
1392
  const enqueue = useCallback(
878
- (action: ComputerAction, frame: ComputerFrame | null, after?: () => Promise<void>) => {
1393
+ (
1394
+ action: ComputerAction,
1395
+ frame: ComputerFrame | null,
1396
+ after?: (isCurrent: () => boolean) => Promise<void>,
1397
+ ) => {
1398
+ const epoch = actionQueueEpochRef.current;
1399
+ const dispatch = actionRef.current;
1400
+ const isCurrent = () => mountedRef.current && epoch === actionQueueEpochRef.current;
879
1401
  actionTailRef.current = actionTailRef.current
880
1402
  .catch(() => undefined)
881
1403
  .then(async () => {
882
- await actionRef.current(action, frame);
883
- await after?.();
1404
+ if (!isCurrent()) return;
1405
+ await dispatch(action, frame);
1406
+ if (!isCurrent()) return;
1407
+ await after?.(isCurrent);
884
1408
  })
885
- .catch((cause) => errorRef.current(cause));
1409
+ .catch((cause) => {
1410
+ if (!isCurrent()) return;
1411
+ actionQueueEpochRef.current += 1;
1412
+ clearBufferedInput();
1413
+ errorRef.current(cause);
1414
+ });
886
1415
  },
887
- [],
1416
+ [clearBufferedInput],
888
1417
  );
889
1418
 
890
1419
  const point = useCallback(
@@ -919,21 +1448,45 @@ function ComputerViewport(props: {
919
1448
  );
920
1449
  }, [enqueue]);
921
1450
 
1451
+ const flushPendingWheel = useCallback(() => {
1452
+ const batch = wheelRef.current;
1453
+ if (!batch) return;
1454
+ if (batch.timer) clearTimeout(batch.timer);
1455
+ wheelRef.current = null;
1456
+ enqueue(
1457
+ {
1458
+ type: "pointer",
1459
+ frameId: batch.frame.frameId,
1460
+ action: "scroll",
1461
+ x: batch.x,
1462
+ y: batch.y,
1463
+ deltaX: batch.deltaX,
1464
+ deltaY: batch.deltaY,
1465
+ },
1466
+ batch.frame,
1467
+ );
1468
+ }, [enqueue]);
1469
+
922
1470
  const pointerDown = (event: PointerEvent<HTMLCanvasElement>) => {
923
- if (!props.frame || props.mutating || !rawInputEnabled || event.button !== 0) return;
1471
+ const frame = paintedFrameRef.current;
1472
+ if (!frame || props.mutating || !rawInputEnabled || event.button !== 0) return;
1473
+ // Preserve the keyboard sink's focus through the canvas pointer default action.
1474
+ event.preventDefault();
1475
+ if (keyboardInputEnabled) inputRef.current?.focus({ preventScroll: true });
1476
+ if (!pointerInputEnabled) return;
1477
+ flushPendingWheel();
924
1478
  flushPendingText();
925
1479
  pointerStartRef.current = {
926
1480
  x: event.clientX,
927
1481
  y: event.clientY,
928
1482
  pointerId: event.pointerId,
929
- frame: props.frame,
1483
+ frame,
930
1484
  };
931
1485
  event.currentTarget.setPointerCapture?.(event.pointerId);
932
- inputRef.current?.focus({ preventScroll: true });
933
1486
  };
934
1487
 
935
1488
  const pointerUp = (event: PointerEvent<HTMLCanvasElement>) => {
936
- if (!rawInputEnabled) return;
1489
+ if (!pointerInputEnabled) return;
937
1490
  const start = pointerStartRef.current;
938
1491
  pointerStartRef.current = null;
939
1492
  if (!start || start.pointerId !== event.pointerId) return;
@@ -998,9 +1551,10 @@ function ComputerViewport(props: {
998
1551
 
999
1552
  const contextMenu = (event: MouseEvent<HTMLCanvasElement>) => {
1000
1553
  event.preventDefault();
1001
- if (!rawInputEnabled) return;
1002
- const frame = props.frame;
1554
+ if (!pointerInputEnabled) return;
1555
+ const frame = paintedFrameRef.current;
1003
1556
  if (!frame) return;
1557
+ flushPendingWheel();
1004
1558
  flushPendingText();
1005
1559
  flushPendingClick();
1006
1560
  const at = point(frame, event.clientX, event.clientY);
@@ -1020,45 +1574,43 @@ function ComputerViewport(props: {
1020
1574
  };
1021
1575
 
1022
1576
  const wheel = (event: WheelEvent<HTMLCanvasElement>) => {
1023
- if (!rawInputEnabled) return;
1024
- const frame = props.frame;
1577
+ if (!pointerInputEnabled) return;
1578
+ const frame = paintedFrameRef.current;
1025
1579
  if (!frame) return;
1026
1580
  const at = point(frame, event.clientX, event.clientY);
1027
1581
  if (!at) return;
1028
1582
  flushPendingText();
1029
1583
  flushPendingClick();
1030
1584
  event.preventDefault();
1031
- const pending = wheelRef.current;
1032
- if (pending?.timer) clearTimeout(pending.timer);
1585
+ let pending = wheelRef.current;
1586
+ if (
1587
+ pending &&
1588
+ (!sameComputerTarget(pending.frame, frame) ||
1589
+ Math.hypot(pending.x - at.x, pending.y - at.y) > 6)
1590
+ ) {
1591
+ flushPendingWheel();
1592
+ pending = null;
1593
+ }
1033
1594
  wheelRef.current = {
1034
1595
  x: at.x,
1035
1596
  y: at.y,
1036
- deltaX: (pending && sameFrameFence(pending.frame, frame) ? pending.deltaX : 0) + event.deltaX,
1037
- deltaY: (pending && sameFrameFence(pending.frame, frame) ? pending.deltaY : 0) + event.deltaY,
1597
+ deltaX: (pending?.deltaX ?? 0) + event.deltaX,
1598
+ deltaY: (pending?.deltaY ?? 0) + event.deltaY,
1038
1599
  frame,
1039
- timer: setTimeout(() => {
1040
- const batch = wheelRef.current;
1041
- wheelRef.current = null;
1042
- if (batch) {
1043
- enqueue(
1044
- {
1045
- type: "pointer",
1046
- frameId: batch.frame.frameId,
1047
- action: "scroll",
1048
- x: batch.x,
1049
- y: batch.y,
1050
- deltaX: batch.deltaX,
1051
- deltaY: batch.deltaY,
1052
- },
1053
- batch.frame,
1054
- );
1055
- }
1056
- }, 45),
1600
+ timer: pending?.timer ?? setTimeout(flushPendingWheel, 45),
1057
1601
  };
1058
1602
  };
1059
1603
 
1060
1604
  const keyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
1061
- if (!rawInputEnabled) return;
1605
+ if (!keyboardInputEnabled) return;
1606
+ if (
1607
+ composingRef.current ||
1608
+ event.nativeEvent.isComposing ||
1609
+ event.nativeEvent.keyCode === 229
1610
+ ) {
1611
+ return;
1612
+ }
1613
+ flushPendingWheel();
1062
1614
  const command = event.metaKey || event.ctrlKey;
1063
1615
  if (
1064
1616
  props.clipboardEnabled &&
@@ -1078,33 +1630,43 @@ function ComputerViewport(props: {
1078
1630
  };
1079
1631
 
1080
1632
  const copy = (event: ClipboardEvent<HTMLTextAreaElement>) => {
1081
- if (!props.clipboardEnabled || !rawInputEnabled) return;
1633
+ if (!props.clipboardEnabled || !keyboardInputEnabled) return;
1082
1634
  event.preventDefault();
1635
+ flushPendingWheel();
1083
1636
  flushPendingText();
1084
1637
  flushPendingClick();
1085
- enqueue({ type: "clipboard", operation: "copy" }, null, async () => {
1086
- const clipboard = await readClipboardRef.current();
1638
+ const readClipboard = readClipboardRef.current;
1639
+ enqueue({ type: "clipboard", operation: "copy" }, null, async (isCurrent) => {
1640
+ const clipboard = await readClipboard();
1641
+ if (!isCurrent()) return;
1087
1642
  if (!clipboard.text) return;
1088
1643
  if (!(await copyTextToClipboard(clipboard.text))) {
1089
- throw new Error("Computer text could not be copied to the local clipboard");
1644
+ throw new Error("Desktop text could not be copied to the local clipboard");
1090
1645
  }
1091
1646
  });
1092
1647
  };
1093
1648
 
1094
1649
  const paste = (event: ClipboardEvent<HTMLTextAreaElement>) => {
1095
- if (!props.clipboardEnabled || !rawInputEnabled) return;
1650
+ if (!props.clipboardEnabled || !keyboardInputEnabled) return;
1096
1651
  event.preventDefault();
1652
+ flushPendingWheel();
1097
1653
  flushPendingText();
1098
1654
  flushPendingClick();
1099
1655
  const text = event.clipboardData.getData("text/plain");
1100
- enqueue({ type: "clipboard", operation: "write", text }, null, async () => {
1101
- assertExactComputerClipboard(await readClipboardRef.current(), text);
1102
- await actionRef.current({ type: "clipboard", operation: "paste" }, null);
1656
+ const readClipboard = readClipboardRef.current;
1657
+ const dispatch = actionRef.current;
1658
+ enqueue({ type: "clipboard", operation: "write", text }, null, async (isCurrent) => {
1659
+ const clipboard = await readClipboard();
1660
+ if (!isCurrent()) return;
1661
+ assertExactComputerClipboard(clipboard, text);
1662
+ await dispatch({ type: "clipboard", operation: "paste" }, null);
1103
1663
  });
1104
1664
  };
1105
1665
 
1106
- const input = (value: string) => {
1107
- if (!value || !rawInputEnabled) return;
1666
+ const input = (value: string, nativeComposing = false) => {
1667
+ if (composingRef.current || nativeComposing) return;
1668
+ if (!value || !keyboardInputEnabled) return;
1669
+ flushPendingWheel();
1108
1670
  flushPendingClick();
1109
1671
  const pending = pendingTextRef.current;
1110
1672
  if (pending) {
@@ -1120,7 +1682,17 @@ function ComputerViewport(props: {
1120
1682
  if (inputRef.current) inputRef.current.value = "";
1121
1683
  };
1122
1684
 
1123
- const showCanvas = props.frame !== null && !props.machineLocked;
1685
+ const compositionEnd = (event: CompositionEvent<HTMLTextAreaElement>) => {
1686
+ composingRef.current = false;
1687
+ input(event.currentTarget.value || event.data);
1688
+ };
1689
+
1690
+ const showCanvas =
1691
+ props.frame !== null &&
1692
+ paintedFrame !== null &&
1693
+ sameComputerTarget(props.frame, paintedFrame) &&
1694
+ !props.machineLocked &&
1695
+ !streamFailed;
1124
1696
  return (
1125
1697
  <div className="relative min-h-0 flex-1 overflow-hidden bg-black">
1126
1698
  <canvas
@@ -1128,7 +1700,7 @@ function ComputerViewport(props: {
1128
1700
  className={cn(
1129
1701
  "absolute inset-0 m-auto max-h-full max-w-full touch-none focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent",
1130
1702
  !showCanvas && "invisible",
1131
- !rawInputEnabled && "cursor-default",
1703
+ !pointerInputEnabled && "cursor-default",
1132
1704
  )}
1133
1705
  onPointerDown={pointerDown}
1134
1706
  onPointerUp={pointerUp}
@@ -1137,26 +1709,34 @@ function ComputerViewport(props: {
1137
1709
  }}
1138
1710
  onContextMenu={contextMenu}
1139
1711
  onWheel={wheel}
1140
- aria-label={`Interactive ${props.target?.kind ?? "computer"} view`}
1712
+ aria-label={`Interactive ${props.target?.kind ?? "desktop"} view`}
1141
1713
  />
1142
1714
  <textarea
1143
1715
  ref={inputRef}
1144
1716
  defaultValue=""
1145
- onInput={(event) => input(event.currentTarget.value)}
1717
+ onInput={(event) =>
1718
+ input(event.currentTarget.value, (event.nativeEvent as InputEvent).isComposing)
1719
+ }
1720
+ onCompositionStart={() => {
1721
+ composingRef.current = true;
1722
+ }}
1723
+ onCompositionEnd={compositionEnd}
1146
1724
  onKeyDown={keyDown}
1147
1725
  onCopy={copy}
1148
1726
  onPaste={paste}
1149
1727
  className="pointer-events-none absolute left-1/2 top-1/2 size-px resize-none overflow-hidden opacity-0"
1150
- aria-label="Computer keyboard input"
1728
+ aria-label="Desktop keyboard input"
1151
1729
  autoCapitalize="off"
1152
1730
  autoCorrect="off"
1153
1731
  spellCheck={false}
1154
- disabled={!rawInputEnabled}
1732
+ disabled={!keyboardInputEnabled}
1155
1733
  />
1156
1734
  {!showCanvas ? (
1157
1735
  <ComputerViewportFallback
1158
1736
  observation={props.observation}
1159
1737
  machineLocked={props.machineLocked}
1738
+ controlUnavailable={props.controlUnavailable}
1739
+ inputAllowed={props.inputAllowed}
1160
1740
  connectionState={props.connectionState}
1161
1741
  error={props.connectionError}
1162
1742
  onAction={(action) => enqueue(action, null)}
@@ -1168,17 +1748,25 @@ function ComputerViewport(props: {
1168
1748
  <LoaderCircleIcon className="size-3 animate-spin" /> Acting
1169
1749
  </div>
1170
1750
  ) : null}
1171
- {showCanvas && !rawInputEnabled ? (
1172
- <div className="absolute bottom-3 left-1/2 flex -translate-x-1/2 items-center gap-2 rounded-full border border-white/10 bg-black/70 py-1 pl-3 pr-1 text-[11px] text-white/80 backdrop-blur">
1173
- <span>Background view · use native controls</span>
1174
- {props.target ? (
1751
+ {showCanvas && (!rawInputEnabled || !props.pointerInput || !props.keyboardInput) ? (
1752
+ <div className="absolute bottom-3 left-1/2 flex w-max max-w-[calc(100%-1.5rem)] -translate-x-1/2 items-center gap-2 rounded-full border border-white/10 bg-black/70 py-1 pl-3 pr-1 text-[11px] text-white/80 backdrop-blur">
1753
+ <span>
1754
+ {!rawInputEnabled && (props.pointerInput || props.keyboardInput)
1755
+ ? "Background view · use app controls"
1756
+ : !props.pointerInput && !props.keyboardInput
1757
+ ? "View only · mouse and keyboard unavailable"
1758
+ : props.keyboardInput
1759
+ ? "Keyboard only · mouse unavailable"
1760
+ : "Mouse only · keyboard unavailable"}
1761
+ </span>
1762
+ {!rawInputEnabled && props.target && (props.pointerInput || props.keyboardInput) ? (
1175
1763
  <button
1176
1764
  type="button"
1177
1765
  disabled={props.mutating}
1178
1766
  onClick={() => enqueue({ type: "focus", targetId: props.target!.id }, null)}
1179
- className="rounded-full bg-white/10 px-2 py-0.5 font-medium text-white transition hover:bg-white/20 disabled:opacity-50"
1767
+ className="shrink-0 whitespace-nowrap rounded-full bg-white/10 px-2 py-0.5 font-medium text-white transition hover:bg-white/20 disabled:opacity-50"
1180
1768
  >
1181
- Control directly
1769
+ Bring to front
1182
1770
  </button>
1183
1771
  ) : null}
1184
1772
  </div>
@@ -1189,13 +1777,15 @@ function ComputerViewport(props: {
1189
1777
 
1190
1778
  function assertExactComputerClipboard(clipboard: ComputerClipboard, expected: string): void {
1191
1779
  if (clipboard.truncated || clipboard.text !== expected) {
1192
- throw new Error("Computer clipboard did not accept the exact pasted text");
1780
+ throw new Error("Desktop clipboard did not accept the exact pasted text");
1193
1781
  }
1194
1782
  }
1195
1783
 
1196
1784
  function ComputerViewportFallback(props: {
1197
1785
  observation: ComputerObservation | null;
1198
1786
  machineLocked: boolean;
1787
+ controlUnavailable: boolean;
1788
+ inputAllowed: boolean;
1199
1789
  connectionState: string;
1200
1790
  error: Error | null;
1201
1791
  onAction: (action: ComputerAction) => void;
@@ -1205,36 +1795,60 @@ function ComputerViewportFallback(props: {
1205
1795
  return (
1206
1796
  <div className="absolute inset-0 grid place-items-center bg-og-bg p-6">
1207
1797
  <div className="max-w-sm text-center">
1208
- <LockKeyholeIcon className="mx-auto size-6 text-og-muted" />
1798
+ <LockKeyholeIcon className="mx-auto size-6 text-og-fg-muted" />
1209
1799
  <p className="mt-3 text-og-menu font-medium text-og-fg">Machine locked</p>
1210
- <p className="mt-1 text-og-control leading-5 text-og-muted">
1800
+ <p className="mt-1 text-og-control leading-5 text-og-fg-muted">
1211
1801
  Live frames and input stay private until someone unlocks the machine.
1212
1802
  </p>
1213
1803
  </div>
1214
1804
  </div>
1215
1805
  );
1216
1806
  }
1217
- const interactive = semanticNodes(props.observation)
1807
+ const appControls = props.observation?.target.kind === "app";
1808
+ const controlFailure = interactionControlFailureFromError(props.error);
1809
+ const interactive = semanticControls(props.observation)
1218
1810
  .filter((node) => semanticAction(node) !== null)
1219
1811
  .slice(0, 10);
1220
1812
  return (
1221
1813
  <div className="absolute inset-0 grid place-items-center bg-og-bg p-6">
1222
- <div className="w-full max-w-md rounded-og-lg border border-og-border bg-og-surface-0 p-4 shadow-lg">
1814
+ <div className="w-full max-w-md rounded-og-lg border border-og-border bg-og-surface-1 p-4 shadow-lg">
1223
1815
  <div className="flex items-center gap-2">
1224
1816
  {props.error ? (
1225
- <CircleAlertIcon className="size-4 text-og-status-error" />
1817
+ <CircleAlertIcon className="size-4 text-og-danger" />
1818
+ ) : appControls ? (
1819
+ <PanelsTopLeftIcon className="size-4 text-og-fg-muted" />
1226
1820
  ) : (
1227
- <LoaderCircleIcon className="size-4 animate-spin text-og-muted" />
1821
+ <LoaderCircleIcon className="size-4 animate-spin text-og-fg-muted" />
1228
1822
  )}
1229
1823
  <p className="text-og-menu font-medium text-og-fg">
1230
- {props.error
1231
- ? "Live view disconnected"
1232
- : computerConnectionLabel(props.connectionState)}
1824
+ {isAttachedChromeGenerationLossError(props.error)
1825
+ ? "Chrome reconnected—open a fresh browser/desktop."
1826
+ : !props.inputAllowed
1827
+ ? "View only"
1828
+ : props.controlUnavailable
1829
+ ? "Desktop controls unavailable"
1830
+ : props.error
1831
+ ? "Live view disconnected"
1832
+ : appControls
1833
+ ? "App controls"
1834
+ : computerConnectionLabel(props.connectionState)}
1233
1835
  </p>
1234
1836
  </div>
1235
- {interactive.length > 0 ? (
1837
+ {props.error ? (
1838
+ <p className="mt-2 text-og-control leading-5 text-og-fg-muted">
1839
+ {isAttachedChromeGenerationLossError(props.error)
1840
+ ? "Chrome reconnected—open a fresh browser/desktop. Use Browser → New browser → Connected Chrome."
1841
+ : (controlFailure?.message ?? props.error.message)}
1842
+ </p>
1843
+ ) : null}
1844
+ {!props.inputAllowed ? (
1845
+ <p className="mt-2 text-og-control leading-5 text-og-fg-muted">
1846
+ Refresh to check desktop controls.
1847
+ </p>
1848
+ ) : null}
1849
+ {interactive.length > 0 && !props.controlUnavailable && props.inputAllowed ? (
1236
1850
  <div className="mt-3 border-t border-og-border pt-3">
1237
- <p className="mb-2 text-og-xs text-og-subtle">Native controls remain available</p>
1851
+ <p className="mb-2 text-og-xs text-og-fg-subtle">App controls remain available</p>
1238
1852
  <div className="flex flex-wrap gap-1.5">
1239
1853
  {interactive.map((node) => (
1240
1854
  <button
@@ -1252,13 +1866,13 @@ function ComputerViewportFallback(props: {
1252
1866
  </div>
1253
1867
  </div>
1254
1868
  ) : null}
1255
- {props.error ? (
1869
+ {props.error && !isAttachedChromeGenerationLossError(props.error) ? (
1256
1870
  <button
1257
1871
  type="button"
1258
1872
  onClick={props.onReconnect}
1259
1873
  className="mt-3 text-og-control font-medium text-og-accent hover:underline"
1260
1874
  >
1261
- Reconnect
1875
+ {controlFailure?.retryable === false ? "Try again" : "Reconnect"}
1262
1876
  </button>
1263
1877
  ) : null}
1264
1878
  </div>
@@ -1269,18 +1883,31 @@ function ComputerViewportFallback(props: {
1269
1883
  function ComputerSemanticPanel(props: {
1270
1884
  observation: ComputerObservation | null;
1271
1885
  mutating: boolean;
1886
+ controlUnavailable: boolean;
1887
+ inputAllowed: boolean;
1272
1888
  onAction: (action: ComputerAction) => void;
1273
1889
  }) {
1274
- const nodes = semanticNodes(props.observation).slice(0, 100);
1890
+ const nodes = semanticControls(props.observation).slice(0, 100);
1275
1891
  return (
1276
- <aside className="w-64 shrink-0 overflow-y-auto border-l border-og-border bg-og-surface-0 p-2">
1277
- <div className="mb-2 flex items-center gap-1.5 px-1 text-og-xs font-medium uppercase tracking-[0.1em] text-og-subtle">
1278
- <KeyboardIcon className="size-3" /> Native controls
1892
+ <aside className="box-border max-h-[40%] w-full shrink-0 overflow-y-auto border-t border-og-border bg-og-surface-1 p-2 @xl/computer-viewer:max-h-none @xl/computer-viewer:w-64 @xl/computer-viewer:border-t-0 @xl/computer-viewer:border-l">
1893
+ <div className="mb-2 flex items-center gap-1.5 px-1 text-og-xs font-medium uppercase tracking-[0.1em] text-og-fg-subtle">
1894
+ <KeyboardIcon className="size-3" /> App controls
1279
1895
  </div>
1280
- {nodes.length === 0 ? (
1281
- <p className="px-1 py-2 text-og-control leading-5 text-og-muted">
1282
- This view has no accessibility controls. Use the live image instead.
1896
+ {!props.inputAllowed ? (
1897
+ <p className="px-1 py-2 text-og-control leading-5 text-og-fg-muted">
1898
+ View only · refresh to check desktop controls.
1899
+ </p>
1900
+ ) : props.controlUnavailable ? (
1901
+ <p className="px-1 py-2 text-og-control leading-5 text-og-fg-muted">
1902
+ Reconnect to use app controls.
1283
1903
  </p>
1904
+ ) : null}
1905
+ {nodes.length === 0 ? (
1906
+ props.controlUnavailable ? null : (
1907
+ <p className="px-1 py-2 text-og-control leading-5 text-og-fg-muted">
1908
+ This view has no accessibility controls. Use the live image instead.
1909
+ </p>
1910
+ )
1284
1911
  ) : (
1285
1912
  <div className="space-y-0.5">
1286
1913
  {nodes.map((node) => (
@@ -1318,6 +1945,7 @@ function ComputerSemanticControl(props: {
1318
1945
  className="rounded-og-sm px-2 py-1.5 hover:bg-og-surface-2"
1319
1946
  onSubmit={(event) => {
1320
1947
  event.preventDefault();
1948
+ if (props.mutating) return;
1321
1949
  props.onAction({
1322
1950
  type: "semantic",
1323
1951
  locator: { kind: "ref", ref: node.ref },
@@ -1326,7 +1954,7 @@ function ComputerSemanticControl(props: {
1326
1954
  });
1327
1955
  }}
1328
1956
  >
1329
- <label className="block truncate text-[10px] uppercase text-og-subtle">
1957
+ <label className="block truncate text-[10px] uppercase text-og-fg-subtle">
1330
1958
  {node.name || node.identifier || node.role}
1331
1959
  </label>
1332
1960
  <div className="mt-1 flex gap-1">
@@ -1358,7 +1986,7 @@ function ComputerSemanticControl(props: {
1358
1986
  }}
1359
1987
  className="flex w-full items-start gap-2 rounded-og-sm px-2 py-1.5 text-left transition hover:bg-og-surface-2 disabled:cursor-default disabled:hover:bg-transparent"
1360
1988
  >
1361
- <span className="mt-0.5 text-[10px] uppercase text-og-subtle">{node.role}</span>
1989
+ <span className="mt-0.5 text-[10px] uppercase text-og-fg-subtle">{node.role}</span>
1362
1990
  <span className="min-w-0 flex-1 truncate text-og-control text-og-fg">
1363
1991
  {node.name || semanticValue(node) || node.identifier || "Unnamed control"}
1364
1992
  </span>
@@ -1370,6 +1998,8 @@ function ComputerStatusBar(props: {
1370
1998
  session: ComputerSession | null;
1371
1999
  target: ComputerTarget | null;
1372
2000
  connectionState: string;
2001
+ controlUnavailable: boolean;
2002
+ inputAllowed: boolean;
1373
2003
  refreshing: boolean;
1374
2004
  showControls: boolean;
1375
2005
  controlCount: number;
@@ -1377,7 +2007,7 @@ function ComputerStatusBar(props: {
1377
2007
  }) {
1378
2008
  const screen = props.target?.kind === "screen";
1379
2009
  return (
1380
- <div className="flex h-8 shrink-0 items-center gap-2 border-t border-og-border bg-og-surface-0 px-2 text-og-xs text-og-subtle">
2010
+ <div className="flex h-8 shrink-0 items-center gap-2 border-t border-og-border bg-og-surface-1 px-2 text-og-xs text-og-fg-subtle">
1381
2011
  <span
1382
2012
  className={cn(
1383
2013
  "size-1.5 rounded-full",
@@ -1385,28 +2015,48 @@ function ComputerStatusBar(props: {
1385
2015
  )}
1386
2016
  />
1387
2017
  <span>
1388
- {props.connectionState === "live" ? "Live" : computerConnectionLabel(props.connectionState)}
2018
+ {!props.inputAllowed
2019
+ ? "View only"
2020
+ : props.controlUnavailable
2021
+ ? "Controls unavailable"
2022
+ : props.target?.kind === "app"
2023
+ ? "App controls"
2024
+ : props.connectionState === "live"
2025
+ ? "Live"
2026
+ : computerConnectionLabel(props.connectionState)}
1389
2027
  </span>
1390
2028
  <span className="min-w-0 flex-1 truncate">
1391
- {screen
1392
- ? "Full screen · input may move pointer and focus"
1393
- : props.session?.capabilities?.backgroundActions
1394
- ? "Window · semantic controls stay in the background"
1395
- : (props.target?.kind ?? "Computer")}
2029
+ {!props.inputAllowed
2030
+ ? "Refresh to check desktop controls"
2031
+ : props.controlUnavailable
2032
+ ? "Reconnect to use desktop input"
2033
+ : screen
2034
+ ? "Full screen · input may move pointer and focus"
2035
+ : props.target?.kind === "app"
2036
+ ? props.session?.capabilities?.backgroundActions
2037
+ ? "App · controls work in the background"
2038
+ : "App controls"
2039
+ : props.session?.capabilities?.backgroundInput
2040
+ ? "Window · clicks and typing stay in the background"
2041
+ : props.session?.capabilities?.backgroundActions
2042
+ ? "Window · app controls work in the background"
2043
+ : (props.target?.kind ?? "Desktop")}
1396
2044
  </span>
1397
2045
  {screen ? <MousePointer2Icon className="size-3" aria-hidden /> : null}
1398
2046
  {props.refreshing ? <LoaderCircleIcon className="size-3 animate-spin" /> : null}
1399
- <button
1400
- type="button"
1401
- onClick={props.onToggleControls}
1402
- aria-pressed={props.showControls}
1403
- className={cn(
1404
- "rounded px-1.5 py-0.5 transition hover:bg-og-surface-2 hover:text-og-fg",
1405
- props.showControls && "bg-og-surface-2 text-og-fg",
1406
- )}
1407
- >
1408
- Controls {props.controlCount}
1409
- </button>
2047
+ {props.controlCount > 0 || props.showControls ? (
2048
+ <button
2049
+ type="button"
2050
+ onClick={props.onToggleControls}
2051
+ aria-pressed={props.showControls}
2052
+ className={cn(
2053
+ "rounded px-1.5 py-0.5 transition hover:bg-og-surface-2 hover:text-og-fg",
2054
+ props.showControls && "bg-og-surface-2 text-og-fg",
2055
+ )}
2056
+ >
2057
+ Controls {props.controlCount}
2058
+ </button>
2059
+ ) : null}
1410
2060
  </div>
1411
2061
  );
1412
2062
  }
@@ -1415,7 +2065,7 @@ function ComputerNotice(props: { icon: ReactNode; text: string; className?: stri
1415
2065
  return (
1416
2066
  <div
1417
2067
  className={cn(
1418
- "grid h-full place-items-center bg-og-bg text-og-control text-og-muted",
2068
+ "grid h-full place-items-center bg-og-bg text-og-control text-og-fg-muted",
1419
2069
  props.className,
1420
2070
  )}
1421
2071
  >
@@ -1427,11 +2077,37 @@ function ComputerNotice(props: { icon: ReactNode; text: string; className?: stri
1427
2077
  );
1428
2078
  }
1429
2079
 
1430
- function semanticNodes(observation: ComputerObservation | null): InteractionSemanticNode[] {
2080
+ const structuralRoles = new Set([
2081
+ "frame",
2082
+ "panel",
2083
+ "section",
2084
+ "document",
2085
+ "document web",
2086
+ "static",
2087
+ "static text",
2088
+ "heading",
2089
+ "paragraph",
2090
+ "notification",
2091
+ "separator",
2092
+ "tool bar",
2093
+ "page tab list",
2094
+ ]);
2095
+
2096
+ function semanticControls(observation: ComputerObservation | null): InteractionSemanticNode[] {
1431
2097
  if (observation?.semantic?.kind !== "snapshot") return [];
1432
2098
  const result: InteractionSemanticNode[] = [];
1433
2099
  const visit = (node: InteractionSemanticNode) => {
1434
- result.push(node);
2100
+ const role = node.role.trim().toLowerCase();
2101
+ const label = node.name?.trim() || node.identifier?.trim();
2102
+ if (
2103
+ node.actions.includes("set_value") ||
2104
+ (label &&
2105
+ label.toLowerCase() !== role &&
2106
+ !structuralRoles.has(role) &&
2107
+ semanticAction(node) !== null)
2108
+ ) {
2109
+ result.push(node);
2110
+ }
1435
2111
  node.children?.forEach(visit);
1436
2112
  };
1437
2113
  observation.semantic.roots.forEach(visit);
@@ -1463,18 +2139,33 @@ function paintCanvas(
1463
2139
  width: number,
1464
2140
  height: number,
1465
2141
  ): void {
2142
+ const dimensionsChanged = canvas.width !== width || canvas.height !== height;
1466
2143
  canvas.width = width;
1467
2144
  canvas.height = height;
1468
2145
  const context = canvas.getContext("2d", { alpha: false });
1469
- if (!context) throw new Error("Computer canvas is unavailable.");
2146
+ if (!context) throw new Error("Desktop canvas is unavailable.");
1470
2147
  context.drawImage(image, 0, 0, width, height);
2148
+ if (dimensionsChanged) fitComputerCanvas(canvas);
2149
+ }
2150
+
2151
+ function fitComputerCanvas(canvas: HTMLCanvasElement): void {
2152
+ const viewport = canvas.parentElement;
2153
+ if (!viewport || canvas.width <= 0 || canvas.height <= 0) return;
2154
+ const scale = Math.min(
2155
+ viewport.clientWidth / canvas.width,
2156
+ viewport.clientHeight / canvas.height,
2157
+ );
2158
+ // Hidden docks retain their previous fit until their content box is visible.
2159
+ if (!Number.isFinite(scale) || scale <= 0) return;
2160
+ canvas.style.width = `${canvas.width * scale}px`;
2161
+ canvas.style.height = `${canvas.height * scale}px`;
1471
2162
  }
1472
2163
 
1473
2164
  function loadImage(url: string): Promise<HTMLImageElement> {
1474
2165
  return new Promise((resolve, reject) => {
1475
2166
  const image = new Image();
1476
2167
  image.onload = () => resolve(image);
1477
- image.onerror = () => reject(new Error("Computer frame image could not be decoded."));
2168
+ image.onerror = () => reject(new Error("Desktop frame image could not be decoded."));
1478
2169
  image.src = url;
1479
2170
  });
1480
2171
  }
@@ -1528,9 +2219,13 @@ export function computerKey(
1528
2219
  }
1529
2220
 
1530
2221
  function sameFrameFence(left: ComputerFrame, right: ComputerFrame): boolean {
2222
+ return left.frameId === right.frameId && sameComputerTarget(left, right);
2223
+ }
2224
+
2225
+ function sameComputerTarget(left: ComputerFrame, right: ComputerFrame): boolean {
1531
2226
  return (
1532
- left.frameId === right.frameId &&
1533
2227
  left.computerSessionId === right.computerSessionId &&
2228
+ left.controllerGeneration === right.controllerGeneration &&
1534
2229
  left.targetId === right.targetId &&
1535
2230
  left.targetGeneration === right.targetGeneration
1536
2231
  );
@@ -1545,14 +2240,31 @@ function computerFailureMessage(session: ComputerSession): string | null {
1545
2240
  case "machine_locked":
1546
2241
  return "Unlock the connected Mac, then try again.";
1547
2242
  case "controller_heartbeat_expired":
1548
- return "The computer connection was lost. Try opening it again.";
2243
+ return "The desktop connection was lost. Try opening it again.";
2244
+ case "controller_transition_expired":
2245
+ return session.placement.kind === "attached_device"
2246
+ ? "Chrome reconnected—open a fresh browser/desktop. Use Browser → New browser → Connected Chrome."
2247
+ : "The desktop connection expired. Open a new desktop.";
1549
2248
  case null:
1550
2249
  return null;
1551
2250
  default:
1552
- return "The computer could not be opened. Try again.";
2251
+ return "The desktop could not be opened. Try again.";
1553
2252
  }
1554
2253
  }
1555
2254
 
2255
+ function isAttachedChromeGenerationLoss(session: ComputerSession): boolean {
2256
+ return (
2257
+ session.lifecycle === "lost" &&
2258
+ session.failureCode === "controller_transition_expired" &&
2259
+ session.placement.kind === "attached_device"
2260
+ );
2261
+ }
2262
+
2263
+ function isAttachedChromeGenerationLossError(error: Error | null): boolean {
2264
+ if (!error) return false;
2265
+ return /placement instance changed|controller authority changed/i.test(error.message);
2266
+ }
2267
+
1556
2268
  function countInterventions(
1557
2269
  interventions: readonly InteractionIntervention[],
1558
2270
  ): Map<string, number> {
@@ -1573,7 +2285,7 @@ function interventionTitle(intervention: InteractionIntervention): string {
1573
2285
  case "confirmation":
1574
2286
  return "Action needed";
1575
2287
  case "other":
1576
- return "Computer needs your help";
2288
+ return "Desktop needs your help";
1577
2289
  }
1578
2290
  }
1579
2291
 
@@ -1586,7 +2298,7 @@ function platformLabel(session: ComputerSession): string {
1586
2298
  case "windows":
1587
2299
  return "Windows";
1588
2300
  default:
1589
- return "Computer";
2301
+ return "Desktop";
1590
2302
  }
1591
2303
  }
1592
2304
 
@@ -1599,31 +2311,31 @@ function placementLabel(session: ComputerSession): string {
1599
2311
  case "attached_device":
1600
2312
  return "Your machine";
1601
2313
  case "external_provider":
1602
- return "Cloud computer";
2314
+ return "Cloud desktop";
1603
2315
  }
1604
2316
  }
1605
2317
 
1606
2318
  function lifecycleLabel(lifecycle: ComputerSession["lifecycle"]): string {
1607
2319
  switch (lifecycle) {
1608
2320
  case "starting":
1609
- return "Opening computer…";
2321
+ return "Opening desktop…";
1610
2322
  case "suspending":
1611
- return "Pausing computer…";
2323
+ return "Pausing desktop…";
1612
2324
  case "suspended":
1613
- return "Computer paused";
2325
+ return "Desktop paused";
1614
2326
  case "restoring":
1615
- return "Restoring computer…";
2327
+ return "Restoring desktop…";
1616
2328
  case "ending":
1617
- return "Closing computer…";
2329
+ return "Closing desktop…";
1618
2330
  default:
1619
- return "Connecting to computer…";
2331
+ return "Connecting to desktop…";
1620
2332
  }
1621
2333
  }
1622
2334
 
1623
2335
  function computerConnectionLabel(state: string): string {
1624
2336
  switch (state) {
1625
2337
  case "attaching":
1626
- return "Opening computer view…";
2338
+ return "Opening desktop view…";
1627
2339
  case "connecting":
1628
2340
  return "Connecting…";
1629
2341
  case "reconnecting":
@@ -1631,6 +2343,6 @@ function computerConnectionLabel(state: string): string {
1631
2343
  case "error":
1632
2344
  return "Disconnected";
1633
2345
  default:
1634
- return "Waiting for computer…";
2346
+ return "Waiting for desktop…";
1635
2347
  }
1636
2348
  }