@opengeni/react 3.3.2 → 3.4.1-canary.38960406141001

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (855) hide show
  1. package/README.md +769 -22
  2. package/dist/accounts.js +2 -0
  3. package/dist/accounts.js.map +1 -1
  4. package/dist/approvals.d.ts +2 -1
  5. package/dist/artifacts-document.js +5 -3
  6. package/dist/artifacts-presentation.js +5 -3
  7. package/dist/artifacts-spreadsheet.d.ts +1 -1
  8. package/dist/artifacts-spreadsheet.js +5 -3
  9. package/dist/artifacts.d.ts +5 -1
  10. package/dist/artifacts.js +536 -42
  11. package/dist/artifacts.js.map +1 -1
  12. package/dist/browser-viewer-ZWR6MWJ7.js +19 -0
  13. package/dist/capability-catalog-row.d.ts +20 -0
  14. package/dist/capability-logo-fallback.d.ts +3 -0
  15. package/dist/chat-interactive-block-4EUKJXUO.js +14 -0
  16. package/dist/chunk-2DDJ6YH6.js +41 -0
  17. package/dist/chunk-2DDJ6YH6.js.map +1 -0
  18. package/dist/chunk-2HVLQ26H.js +183 -0
  19. package/dist/chunk-2HVLQ26H.js.map +1 -0
  20. package/dist/{chunk-XYRDDSXF.js → chunk-2I6R3GCZ.js} +4285 -3980
  21. package/dist/chunk-2I6R3GCZ.js.map +1 -0
  22. package/dist/{chunk-JAQ5K7W6.js → chunk-2SFFYIH3.js} +1178 -166
  23. package/dist/chunk-2SFFYIH3.js.map +1 -0
  24. package/dist/chunk-3PVDYTTB.js +24 -0
  25. package/dist/chunk-3PVDYTTB.js.map +1 -0
  26. package/dist/chunk-3RJKUVCA.js +14068 -0
  27. package/dist/chunk-3RJKUVCA.js.map +1 -0
  28. package/dist/chunk-3UZCWK46.js +298 -0
  29. package/dist/chunk-3UZCWK46.js.map +1 -0
  30. package/dist/chunk-47CKUXTW.js +293 -0
  31. package/dist/chunk-47CKUXTW.js.map +1 -0
  32. package/dist/chunk-4QYP6QUI.js +109 -0
  33. package/dist/chunk-4QYP6QUI.js.map +1 -0
  34. package/dist/chunk-5EY26VZ7.js +1053 -0
  35. package/dist/chunk-5EY26VZ7.js.map +1 -0
  36. package/dist/chunk-5I4VRWBB.js +56 -0
  37. package/dist/chunk-5I4VRWBB.js.map +1 -0
  38. package/dist/chunk-5WOR7UCL.js +191 -0
  39. package/dist/chunk-5WOR7UCL.js.map +1 -0
  40. package/dist/chunk-5YJLGOM5.js +51 -0
  41. package/dist/chunk-5YJLGOM5.js.map +1 -0
  42. package/dist/chunk-6JFN3OWG.js +386 -0
  43. package/dist/chunk-6JFN3OWG.js.map +1 -0
  44. package/dist/chunk-7QJT5O4S.js +35 -0
  45. package/dist/chunk-7QJT5O4S.js.map +1 -0
  46. package/dist/chunk-A55KV72M.js +402 -0
  47. package/dist/chunk-A55KV72M.js.map +1 -0
  48. package/dist/chunk-ADWCLI4G.js +2028 -0
  49. package/dist/chunk-ADWCLI4G.js.map +1 -0
  50. package/dist/chunk-B27E3V37.js +29 -0
  51. package/dist/chunk-B27E3V37.js.map +1 -0
  52. package/dist/chunk-B7TC2A33.js +83 -0
  53. package/dist/chunk-B7TC2A33.js.map +1 -0
  54. package/dist/chunk-BLL56F7R.js +385 -0
  55. package/dist/chunk-BLL56F7R.js.map +1 -0
  56. package/dist/chunk-BOFEQC6K.js +162 -0
  57. package/dist/chunk-BOFEQC6K.js.map +1 -0
  58. package/dist/chunk-CM7LVUKC.js +35 -0
  59. package/dist/chunk-CM7LVUKC.js.map +1 -0
  60. package/dist/chunk-CORUP6JB.js +122 -0
  61. package/dist/chunk-CORUP6JB.js.map +1 -0
  62. package/dist/chunk-CSX5FFKZ.js +35 -0
  63. package/dist/chunk-CSX5FFKZ.js.map +1 -0
  64. package/dist/chunk-EGVHJKGJ.js +3125 -0
  65. package/dist/chunk-EGVHJKGJ.js.map +1 -0
  66. package/dist/{chunk-NEXLLTP4.js → chunk-G76LSA76.js} +6 -5
  67. package/dist/chunk-G76LSA76.js.map +1 -0
  68. package/dist/{chunk-PEI6RANI.js → chunk-GQMRL52F.js} +311 -75
  69. package/dist/chunk-GQMRL52F.js.map +1 -0
  70. package/dist/{chunk-ELIYWBZR.js → chunk-GUUXOISA.js} +1113 -457
  71. package/dist/chunk-GUUXOISA.js.map +1 -0
  72. package/dist/{chunk-HUIKIDM5.js → chunk-I7X2P4FU.js} +225 -116
  73. package/dist/chunk-I7X2P4FU.js.map +1 -0
  74. package/dist/chunk-IGFZFLI2.js +1390 -0
  75. package/dist/chunk-IGFZFLI2.js.map +1 -0
  76. package/dist/{chunk-4MDLEHXL.js → chunk-IMSUVYQB.js} +26 -13
  77. package/dist/chunk-IMSUVYQB.js.map +1 -0
  78. package/dist/chunk-IOYHDEP3.js +38 -0
  79. package/dist/chunk-IOYHDEP3.js.map +1 -0
  80. package/dist/chunk-IZV5FTJC.js +55 -0
  81. package/dist/chunk-IZV5FTJC.js.map +1 -0
  82. package/dist/chunk-KF33GTWN.js +343 -0
  83. package/dist/chunk-KF33GTWN.js.map +1 -0
  84. package/dist/chunk-KSGQ5ODA.js +457 -0
  85. package/dist/chunk-KSGQ5ODA.js.map +1 -0
  86. package/dist/{chunk-ZPBTI3TE.js → chunk-KZC6QRNR.js} +1513 -697
  87. package/dist/chunk-KZC6QRNR.js.map +1 -0
  88. package/dist/chunk-LJUMYUBO.js +218 -0
  89. package/dist/chunk-LJUMYUBO.js.map +1 -0
  90. package/dist/chunk-LSW2OJVV.js +380 -0
  91. package/dist/chunk-LSW2OJVV.js.map +1 -0
  92. package/dist/chunk-M357MHH3.js +2 -0
  93. package/dist/chunk-M357MHH3.js.map +1 -0
  94. package/dist/chunk-MK2TQO4Y.js +24 -0
  95. package/dist/chunk-MK2TQO4Y.js.map +1 -0
  96. package/dist/chunk-N7S72EXK.js +25 -0
  97. package/dist/chunk-N7S72EXK.js.map +1 -0
  98. package/dist/{chunk-YVCHAQZY.js → chunk-NKMYZFHA.js} +616 -621
  99. package/dist/chunk-NKMYZFHA.js.map +1 -0
  100. package/dist/chunk-NX4ADH73.js +14 -0
  101. package/dist/chunk-NX4ADH73.js.map +1 -0
  102. package/dist/chunk-O7F6UJO4.js +71 -0
  103. package/dist/chunk-O7F6UJO4.js.map +1 -0
  104. package/dist/chunk-OYQQJL3J.js +16 -0
  105. package/dist/chunk-OYQQJL3J.js.map +1 -0
  106. package/dist/{chunk-H5YGYNXS.js → chunk-P4BNUO57.js} +60 -17
  107. package/dist/chunk-P4BNUO57.js.map +1 -0
  108. package/dist/{chunk-JVYQRRC4.js → chunk-PEHTX3V7.js} +21 -1
  109. package/dist/chunk-PEHTX3V7.js.map +1 -0
  110. package/dist/chunk-PMV53IMM.js +26 -0
  111. package/dist/chunk-PMV53IMM.js.map +1 -0
  112. package/dist/{chunk-KCGXPP5V.js → chunk-PYEOPA4M.js} +13 -7
  113. package/dist/chunk-PYEOPA4M.js.map +1 -0
  114. package/dist/{chunk-KZ73RL76.js → chunk-QHUE7E4N.js} +8 -3
  115. package/dist/chunk-QHUE7E4N.js.map +1 -0
  116. package/dist/chunk-QV3DJVRZ.js +11 -0
  117. package/dist/chunk-QV3DJVRZ.js.map +1 -0
  118. package/dist/chunk-R5SBNG7S.js +810 -0
  119. package/dist/chunk-R5SBNG7S.js.map +1 -0
  120. package/dist/chunk-RI42H5IU.js +326 -0
  121. package/dist/chunk-RI42H5IU.js.map +1 -0
  122. package/dist/chunk-S2QH6EZ5.js +211 -0
  123. package/dist/chunk-S2QH6EZ5.js.map +1 -0
  124. package/dist/chunk-SHLZMAJN.js +304 -0
  125. package/dist/chunk-SHLZMAJN.js.map +1 -0
  126. package/dist/chunk-TGXK4BAZ.js +293 -0
  127. package/dist/chunk-TGXK4BAZ.js.map +1 -0
  128. package/dist/{chunk-5YK2ZDIG.js → chunk-TNEZNMSN.js} +4 -10
  129. package/dist/chunk-TNEZNMSN.js.map +1 -0
  130. package/dist/{chunk-22KP6LR5.js → chunk-TXS65BB2.js} +3 -1
  131. package/dist/{chunk-22KP6LR5.js.map → chunk-TXS65BB2.js.map} +1 -1
  132. package/dist/chunk-UNJPZGNK.js +63 -0
  133. package/dist/chunk-UNJPZGNK.js.map +1 -0
  134. package/dist/{chunk-IOLVN74D.js → chunk-V5ZUJT2T.js} +175 -42
  135. package/dist/chunk-V5ZUJT2T.js.map +1 -0
  136. package/dist/chunk-WBJY57A3.js +402 -0
  137. package/dist/chunk-WBJY57A3.js.map +1 -0
  138. package/dist/chunk-WIUKB3LD.js +1911 -0
  139. package/dist/chunk-WIUKB3LD.js.map +1 -0
  140. package/dist/{chunk-ICA6R6TW.js → chunk-WNN7NO2N.js} +1451 -355
  141. package/dist/chunk-WNN7NO2N.js.map +1 -0
  142. package/dist/{chunk-3AGBGTRT.js → chunk-X5UVUIMF.js} +1712 -1174
  143. package/dist/chunk-X5UVUIMF.js.map +1 -0
  144. package/dist/chunk-XDA5QJE7.js +402 -0
  145. package/dist/chunk-XDA5QJE7.js.map +1 -0
  146. package/dist/{chunk-VZTQ73XT.js → chunk-XEA5BCMA.js} +3 -1
  147. package/dist/{chunk-VZTQ73XT.js.map → chunk-XEA5BCMA.js.map} +1 -1
  148. package/dist/chunk-XHF7OYAN.js +1150 -0
  149. package/dist/chunk-XHF7OYAN.js.map +1 -0
  150. package/dist/chunk-YSRBFJMQ.js +43 -0
  151. package/dist/chunk-YSRBFJMQ.js.map +1 -0
  152. package/dist/chunk-ZOZAPAUO.js +174 -0
  153. package/dist/chunk-ZOZAPAUO.js.map +1 -0
  154. package/dist/chunk-ZQV5W7YC.js +809 -0
  155. package/dist/chunk-ZQV5W7YC.js.map +1 -0
  156. package/dist/chunk-ZUWF4NF7.js +13 -0
  157. package/dist/chunk-ZUWF4NF7.js.map +1 -0
  158. package/dist/client.d.ts +16 -7
  159. package/dist/clipboard.js +3 -1
  160. package/dist/commands/types.d.ts +2 -0
  161. package/dist/components/anthropic-mark.d.ts +3 -0
  162. package/dist/components/approval-surface.d.ts +19 -6
  163. package/dist/components/artifacts/artifact-chrome.d.ts +120 -0
  164. package/dist/components/artifacts/artifact-sandbox.d.ts +30 -0
  165. package/dist/components/artifacts/artifact-surface.d.ts +4 -2
  166. package/dist/components/artifacts/chat-interactive-block.d.ts +29 -0
  167. package/dist/components/artifacts/deferred-chat-media.d.ts +15 -0
  168. package/dist/components/artifacts/document-editor.d.ts +4 -2
  169. package/dist/components/artifacts/editable-artifact-ui.d.ts +2 -1
  170. package/dist/components/artifacts/editable-artifact-view.d.ts +48 -0
  171. package/dist/components/artifacts/editable-document.d.ts +1 -1
  172. package/dist/components/artifacts/editable-presentation.d.ts +2 -2
  173. package/dist/components/artifacts/editable-spreadsheet.d.ts +8 -3
  174. package/dist/components/artifacts/index.d.ts +9 -2
  175. package/dist/components/artifacts/inline-html-document.d.ts +2 -0
  176. package/dist/components/artifacts/latest-task-runner.d.ts +15 -0
  177. package/dist/components/artifacts/presentation-editor.d.ts +6 -2
  178. package/dist/components/artifacts/published-html-artifact-frame.d.ts +23 -3
  179. package/dist/components/artifacts/session-artifact-viewer.d.ts +39 -0
  180. package/dist/components/artifacts/site-view.d.ts +26 -0
  181. package/dist/components/artifacts/spreadsheet-command-queue.d.ts +20 -0
  182. package/dist/components/artifacts/spreadsheet-download.d.ts +7 -0
  183. package/dist/components/artifacts/spreadsheet-general-display.d.ts +24 -0
  184. package/dist/components/artifacts/spreadsheet-grid.d.ts +10 -3
  185. package/dist/components/artifacts/spreadsheet-resize.d.ts +50 -0
  186. package/dist/components/artifacts/spreadsheet-submissions.d.ts +9 -0
  187. package/dist/components/artifacts/visualization-assets.gen.d.ts +2 -0
  188. package/dist/components/browser-select-control.d.ts +16 -0
  189. package/dist/components/browser-viewer.d.ts +11 -2
  190. package/dist/components/chat-composer.d.ts +21 -3
  191. package/dist/components/child-session-link.d.ts +9 -0
  192. package/dist/components/claude-mark.d.ts +3 -0
  193. package/dist/components/code-editor.d.ts +1 -4
  194. package/dist/components/composer-transcription-control.d.ts +7 -0
  195. package/dist/components/composer.d.ts +34 -67
  196. package/dist/components/computer-viewer.d.ts +1 -1
  197. package/dist/components/copy-button.d.ts +3 -1
  198. package/dist/components/desktop-viewer.d.ts +1 -1
  199. package/dist/components/grok-mark.d.ts +26 -0
  200. package/dist/components/human-input-form.d.ts +28 -32
  201. package/dist/components/human-input-surface.d.ts +7 -2
  202. package/dist/components/linkified-text.d.ts +18 -0
  203. package/dist/components/machine-input-display.d.ts +22 -1
  204. package/dist/components/machines/agent-status-icon.d.ts +6 -0
  205. package/dist/components/machines/machine-detail.d.ts +7 -1
  206. package/dist/components/machines/machine-status.d.ts +48 -0
  207. package/dist/components/markdown-table-layout.d.ts +18 -0
  208. package/dist/components/markdown.d.ts +37 -4
  209. package/dist/components/message-timeline.d.ts +86 -7
  210. package/dist/components/model-mark.d.ts +29 -0
  211. package/dist/components/model-policy-picker-menu.d.ts +9 -0
  212. package/dist/components/model-policy-picker.d.ts +35 -28
  213. package/dist/components/new-conversation.d.ts +32 -0
  214. package/dist/components/open-geni-chat.d.ts +73 -0
  215. package/dist/components/open-geni-links.d.ts +68 -0
  216. package/dist/components/openrouter-mark.d.ts +6 -0
  217. package/dist/components/opper-mark.d.ts +7 -0
  218. package/dist/components/preview-loading.d.ts +2 -0
  219. package/dist/components/provider-recovery-notice.d.ts +16 -0
  220. package/dist/components/queue-item-content.d.ts +9 -0
  221. package/dist/components/rehype-secret-refs.d.ts +23 -0
  222. package/dist/components/remark-soft-line-breaks.d.ts +38 -0
  223. package/dist/components/sandbox-files.d.ts +3 -1
  224. package/dist/components/sandbox-workspace.d.ts +18 -0
  225. package/dist/components/secret-question-field.d.ts +31 -0
  226. package/dist/components/secret-ref-chip.d.ts +35 -0
  227. package/dist/components/session-capability-frame.d.ts +37 -0
  228. package/dist/components/session-chrome.d.ts +57 -8
  229. package/dist/components/session-commands-panel.d.ts +6 -0
  230. package/dist/components/session-commands.d.ts +6 -0
  231. package/dist/components/session-connection-request.d.ts +8 -0
  232. package/dist/components/session-conversation.d.ts +157 -0
  233. package/dist/components/session-list.d.ts +39 -0
  234. package/dist/components/session-mcp-capability-card.d.ts +41 -0
  235. package/dist/components/session-proxy-scope.d.ts +35 -0
  236. package/dist/components/session-status.d.ts +4 -1
  237. package/dist/components/session-usage-limit-notice.d.ts +39 -0
  238. package/dist/components/timeline-anchor.d.ts +24 -0
  239. package/dist/components/timeline-annotation-chrome.d.ts +25 -0
  240. package/dist/components/timeline-annotation-layout.d.ts +42 -0
  241. package/dist/components/timeline-annotation-markers.d.ts +8 -0
  242. package/dist/components/timeline-annotation-reveal-context.d.ts +4 -0
  243. package/dist/components/timeline-annotation-shared.d.ts +31 -0
  244. package/dist/components/timeline-annotations-dialog.d.ts +3 -2
  245. package/dist/components/timeline-annotations.d.ts +13 -3
  246. package/dist/components/timeline-reading-progress.d.ts +11 -0
  247. package/dist/components/timeline-search.d.ts +34 -0
  248. package/dist/components/timeline-settlement.d.ts +16 -0
  249. package/dist/components/tool-action-review.d.ts +47 -0
  250. package/dist/components/tool-review-history.d.ts +25 -0
  251. package/dist/components/usage-limit-notice.d.ts +49 -0
  252. package/dist/components/usage-member-list.d.ts +91 -0
  253. package/dist/components/usage-meter.d.ts +73 -0
  254. package/dist/components/use-viewer-menu-dismiss.d.ts +2 -0
  255. package/dist/components/user-message-body.d.ts +10 -1
  256. package/dist/composer-messages.d.ts +74 -0
  257. package/dist/composer-messages.js +9 -0
  258. package/dist/composer-messages.js.map +1 -0
  259. package/dist/composer-voice-events.d.ts +8 -0
  260. package/dist/composer.d.ts +5 -4
  261. package/dist/composer.js +26 -13
  262. package/dist/computer-viewer-GMLDXPON.js +20 -0
  263. package/dist/computer-viewer-GMLDXPON.js.map +1 -0
  264. package/dist/connect-account-identity.d.ts +8 -0
  265. package/dist/connect-accounts.d.ts +15 -0
  266. package/dist/connect-chooser.d.ts +18 -0
  267. package/dist/connect-panel.d.ts +18 -0
  268. package/dist/connect-setup.d.ts +18 -0
  269. package/dist/connect.d.ts +21 -0
  270. package/dist/connect.js +2149 -0
  271. package/dist/connect.js.map +1 -0
  272. package/dist/connection-catalog.d.ts +38 -0
  273. package/dist/connection-discovery.d.ts +12 -0
  274. package/dist/connection-installed.d.ts +17 -0
  275. package/dist/connection-logo.d.ts +9 -0
  276. package/dist/connection-service-logo.d.ts +8 -0
  277. package/dist/connection-service-presentation.d.ts +13 -0
  278. package/dist/connection-skeleton.d.ts +5 -0
  279. package/dist/connection-type-picker.d.ts +6 -0
  280. package/dist/conversation-timeline.d.ts +5 -0
  281. package/dist/desktop.d.ts +7 -0
  282. package/dist/desktop.js +27 -0
  283. package/dist/desktop.js.map +1 -0
  284. package/dist/device-authorization.d.ts +23 -0
  285. package/dist/diffs.d.ts +8 -0
  286. package/dist/diffs.js +15 -0
  287. package/dist/diffs.js.map +1 -0
  288. package/dist/discovery-cache.d.ts +5 -0
  289. package/dist/editor.d.ts +7 -0
  290. package/dist/editor.js +22 -0
  291. package/dist/editor.js.map +1 -0
  292. package/dist/embedded-new-voice-OCRNT6U5.js +18 -0
  293. package/dist/embedded-new-voice-OCRNT6U5.js.map +1 -0
  294. package/dist/embedded-voice-RPHPYVBT.js +18 -0
  295. package/dist/embedded-voice-RPHPYVBT.js.map +1 -0
  296. package/dist/{fleet-decision-row-GGCAT4E4.js → fleet-decision-row-SI63O5V4.js} +4 -1
  297. package/dist/fleet-decision-row-SI63O5V4.js.map +1 -0
  298. package/dist/hooks/latest-question.d.ts +25 -0
  299. package/dist/hooks/shared-machines-list.d.ts +4 -0
  300. package/dist/hooks/shared-query.d.ts +113 -0
  301. package/dist/hooks/use-attached-browsers.d.ts +1 -1
  302. package/dist/hooks/use-auth-runs.d.ts +1 -4
  303. package/dist/hooks/use-available-models.d.ts +1 -1
  304. package/dist/hooks/use-browser-frame-stream.d.ts +1 -1
  305. package/dist/hooks/use-browser-session.d.ts +12 -1
  306. package/dist/hooks/use-client-config-flags.d.ts +49 -0
  307. package/dist/hooks/use-codex-accounts.d.ts +59 -10
  308. package/dist/hooks/use-composer.d.ts +24 -2
  309. package/dist/hooks/use-computer-frame-stream.d.ts +1 -1
  310. package/dist/hooks/use-computer-input-posture.d.ts +13 -0
  311. package/dist/hooks/use-computer-session.d.ts +4 -0
  312. package/dist/hooks/use-connect.d.ts +23 -0
  313. package/dist/hooks/use-desktop-stream.d.ts +1 -1
  314. package/dist/hooks/use-file-attachments.d.ts +12 -0
  315. package/dist/hooks/use-goal.d.ts +2 -1
  316. package/dist/hooks/use-last-started-turn-policy.d.ts +5 -1
  317. package/dist/hooks/use-new-conversation.d.ts +56 -0
  318. package/dist/hooks/use-new-session-draft.d.ts +74 -0
  319. package/dist/hooks/use-realtime-voice-models.d.ts +7 -0
  320. package/dist/hooks/use-sandbox-files.d.ts +4 -1
  321. package/dist/hooks/use-session-conversation.d.ts +49 -0
  322. package/dist/hooks/use-session-events.d.ts +26 -5
  323. package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
  324. package/dist/hooks/use-session-model-account.d.ts +48 -0
  325. package/dist/hooks/use-supergrok-accounts.d.ts +7 -1
  326. package/dist/hooks/use-terminal-stream.d.ts +11 -5
  327. package/dist/hooks/use-usage.d.ts +40 -0
  328. package/dist/hooks/use-vaults.d.ts +53 -0
  329. package/dist/hooks/use-voice-input.d.ts +8 -1
  330. package/dist/hooks/use-workspace-edit.d.ts +4 -0
  331. package/dist/hooks/use-workspace-sessions.d.ts +24 -5
  332. package/dist/human-input-model.d.ts +122 -0
  333. package/dist/identity-link-accounts.d.ts +11 -0
  334. package/dist/identity-link-consent.d.ts +18 -0
  335. package/dist/index.d.ts +82 -20
  336. package/dist/index.js +2340 -6603
  337. package/dist/index.js.map +1 -1
  338. package/dist/interaction.d.ts +1 -1
  339. package/dist/interaction.js +14 -24
  340. package/dist/interaction.js.map +1 -1
  341. package/dist/latest-question-R77KWSUP.js +118 -0
  342. package/dist/latest-question-R77KWSUP.js.map +1 -0
  343. package/dist/lib/clipboard.d.ts +7 -0
  344. package/dist/lib/error-message.d.ts +7 -0
  345. package/dist/lib/format.d.ts +58 -4
  346. package/dist/lib/host-theme.d.ts +77 -0
  347. package/dist/lib/interaction-errors.d.ts +3 -0
  348. package/dist/lib/menu-styles.d.ts +22 -0
  349. package/dist/lib/notify-observer.d.ts +2 -0
  350. package/dist/lib/pierre-diffs-loader.d.ts +20 -0
  351. package/dist/lib/provider-recovery.d.ts +55 -0
  352. package/dist/lib/scrollbars.d.ts +11 -0
  353. package/dist/lib/terminal-input-readiness.d.ts +11 -0
  354. package/dist/lib/transient-retry.d.ts +25 -0
  355. package/dist/lib/workbench-peers.d.ts +36 -0
  356. package/dist/lib/xterm-theme.d.ts +7 -5
  357. package/dist/machines.js +9 -5
  358. package/dist/markdown-table-layout-3PV2UAAH.js +113 -0
  359. package/dist/markdown-table-layout-3PV2UAAH.js.map +1 -0
  360. package/dist/mcp-connection-status.d.ts +14 -0
  361. package/dist/model-mark-paths.d.ts +34 -0
  362. package/dist/model-mark-paths.js +16 -0
  363. package/dist/model-mark-paths.js.map +1 -0
  364. package/dist/model-picker-model.d.ts +111 -0
  365. package/dist/model-picker-model.js +25 -0
  366. package/dist/model-picker-model.js.map +1 -0
  367. package/dist/model-policy-picker-menu-7GJ5URPR.js +17 -0
  368. package/dist/model-policy-picker-menu-7GJ5URPR.js.map +1 -0
  369. package/dist/model-policy.d.ts +30 -3
  370. package/dist/model-policy.js +14 -1
  371. package/dist/native-previews.d.ts +28 -0
  372. package/dist/native-previews.js +82 -0
  373. package/dist/native-previews.js.map +1 -0
  374. package/dist/new-session-draft.d.ts +7 -0
  375. package/dist/new-session-draft.js +639 -0
  376. package/dist/new-session-draft.js.map +1 -0
  377. package/dist/notification-text.d.ts +27 -0
  378. package/dist/{noto-sans-arabic-loader-IA2T4X2A.js → noto-sans-arabic-loader-XSH2KYWJ.js} +3 -1
  379. package/dist/{noto-sans-arabic-loader-IA2T4X2A.js.map → noto-sans-arabic-loader-XSH2KYWJ.js.map} +1 -1
  380. package/dist/{noto-sans-jp-loader.generated-HFGLYBR3.js → noto-sans-jp-loader.generated-MR5L6V7K.js} +3 -1
  381. package/dist/{noto-sans-jp-loader.generated-HFGLYBR3.js.map → noto-sans-jp-loader.generated-MR5L6V7K.js.map} +1 -1
  382. package/dist/older-history.d.ts +3 -2
  383. package/dist/organization-model.d.ts +32 -0
  384. package/dist/organization-model.js +65 -0
  385. package/dist/organization-model.js.map +1 -0
  386. package/dist/{platform-activity-row-BJHUHXAT.js → platform-activity-row-5OCKOQWZ.js} +16 -78
  387. package/dist/platform-activity-row-5OCKOQWZ.js.map +1 -0
  388. package/dist/plugin-details.d.ts +14 -0
  389. package/dist/plugin-discovery.d.ts +25 -0
  390. package/dist/preview-loading-paint.d.ts +20 -0
  391. package/dist/provider.d.ts +25 -2
  392. package/dist/queue-presentation-model.d.ts +72 -0
  393. package/dist/queue-presentation-model.js +38 -0
  394. package/dist/queue-presentation-model.js.map +1 -0
  395. package/dist/{queue-surface-implementation-BSKUWHRI.js → queue-surface-implementation-GJBH7NKO.js} +65 -265
  396. package/dist/queue-surface-implementation-GJBH7NKO.js.map +1 -0
  397. package/dist/realtime/embedded-new-voice.d.ts +1 -0
  398. package/dist/realtime/embedded-voice.d.ts +1 -0
  399. package/dist/realtime/realtime-control.d.ts +4 -65
  400. package/dist/realtime/session-realtime.d.ts +78 -0
  401. package/dist/realtime.js +25 -1333
  402. package/dist/realtime.js.map +1 -1
  403. package/dist/sandbox-files-O27IPOZW.js +21 -0
  404. package/dist/sandbox-files-O27IPOZW.js.map +1 -0
  405. package/dist/sandbox-label-model.d.ts +30 -0
  406. package/dist/sandbox-label-model.js +20 -0
  407. package/dist/sandbox-label-model.js.map +1 -0
  408. package/dist/scrollbars.d.ts +1 -0
  409. package/dist/scrollbars.js +10 -0
  410. package/dist/scrollbars.js.map +1 -0
  411. package/dist/secret-reference-label.d.ts +14 -0
  412. package/dist/secrets-ui.d.ts +57 -0
  413. package/dist/session-agents-model.d.ts +10 -0
  414. package/dist/session-agents-model.js +25 -0
  415. package/dist/session-agents-model.js.map +1 -0
  416. package/dist/session-attention-model.d.ts +8 -0
  417. package/dist/session-attention-model.js +47 -0
  418. package/dist/session-attention-model.js.map +1 -0
  419. package/dist/session-auth-recommendation.d.ts +5 -0
  420. package/dist/session-capability-policy.d.ts +31 -0
  421. package/dist/session-feedback-model.d.ts +20 -0
  422. package/dist/session-feedback-model.js +56 -0
  423. package/dist/session-feedback-model.js.map +1 -0
  424. package/dist/session-list-model.d.ts +138 -0
  425. package/dist/session-list-model.js +274 -0
  426. package/dist/session-list-model.js.map +1 -0
  427. package/dist/session-realtime-7DJOIHQW.js +21 -0
  428. package/dist/session-realtime-7DJOIHQW.js.map +1 -0
  429. package/dist/session-realtime.d.ts +7 -0
  430. package/dist/session-realtime.js +19 -0
  431. package/dist/session-realtime.js.map +1 -0
  432. package/dist/session-status-model.d.ts +36 -0
  433. package/dist/session-ui.d.ts +39 -1
  434. package/dist/session-ui.js +126 -12
  435. package/dist/session-ui.js.map +1 -1
  436. package/dist/session.d.ts +8 -2
  437. package/dist/session.js +35 -20
  438. package/dist/site-snapshot.d.ts +14 -0
  439. package/dist/sites-ui.d.ts +24 -0
  440. package/dist/sites.d.ts +3 -0
  441. package/dist/sites.js +14 -0
  442. package/dist/sites.js.map +1 -0
  443. package/dist/skill-discovery.d.ts +47 -0
  444. package/dist/terminal.d.ts +8 -0
  445. package/dist/terminal.js +32 -0
  446. package/dist/terminal.js.map +1 -0
  447. package/dist/testing/session-scenarios.d.ts +10 -0
  448. package/dist/testing/session-scenarios.js +367 -0
  449. package/dist/testing/session-scenarios.js.map +1 -0
  450. package/dist/timeline/activity-rail.d.ts +11 -2
  451. package/dist/timeline/agent-identity.d.ts +30 -0
  452. package/dist/timeline/agent-row.d.ts +31 -0
  453. package/dist/timeline/allowance-exhausted-row.d.ts +24 -0
  454. package/dist/timeline/compaction-copy.d.ts +46 -0
  455. package/dist/timeline/focus-sequence.d.ts +9 -0
  456. package/dist/timeline/genie-copy.d.ts +3 -0
  457. package/dist/timeline/genie-loading.d.ts +47 -0
  458. package/dist/timeline/index.d.ts +10 -5
  459. package/dist/timeline/knowledge-receipt.d.ts +17 -0
  460. package/dist/timeline/message-sender.d.ts +19 -0
  461. package/dist/timeline/notice-presentation.d.ts +13 -0
  462. package/dist/timeline/parsers.d.ts +11 -0
  463. package/dist/timeline/patch-apply-command.d.ts +18 -0
  464. package/dist/timeline/platform-activity-presentation.d.ts +21 -0
  465. package/dist/timeline/platform-activity-row.d.ts +1 -2
  466. package/dist/timeline/presented-image.d.ts +11 -0
  467. package/dist/timeline/projection.d.ts +31 -2
  468. package/dist/timeline/provider-logo.d.ts +15 -0
  469. package/dist/timeline/question-nav-model.d.ts +23 -0
  470. package/dist/timeline/registry.d.ts +12 -1
  471. package/dist/timeline/retained-image.d.ts +18 -0
  472. package/dist/timeline/retained-loaders.d.ts +27 -0
  473. package/dist/timeline/rolling-activity.d.ts +11 -0
  474. package/dist/timeline/screenshot-lightbox.d.ts +23 -1
  475. package/dist/timeline/session-usage-limit-row.d.ts +11 -0
  476. package/dist/timeline/shared.d.ts +4 -1
  477. package/dist/timeline/startup-dispatch-details.d.ts +5 -0
  478. package/dist/timeline/startup-preference.d.ts +3 -0
  479. package/dist/timeline/startup-timings.d.ts +4 -0
  480. package/dist/timeline/tool-display-name.d.ts +5 -2
  481. package/dist/timeline/tool-presentation.d.ts +172 -0
  482. package/dist/timeline/turn-summary-model.d.ts +95 -0
  483. package/dist/timeline/turn-summary.d.ts +20 -54
  484. package/dist/timeline/types.d.ts +179 -7
  485. package/dist/timeline/work-presentation.d.ts +60 -0
  486. package/dist/timeline-annotation-markers-U3G66KUD.js +166 -0
  487. package/dist/timeline-annotation-markers-U3G66KUD.js.map +1 -0
  488. package/dist/timeline-annotation-selection-BI7QTPTZ.js +242 -0
  489. package/dist/timeline-annotation-selection-BI7QTPTZ.js.map +1 -0
  490. package/dist/timeline-annotations-dialog-FOMHSDTE.js +274 -0
  491. package/dist/timeline-annotations-dialog-FOMHSDTE.js.map +1 -0
  492. package/dist/timeline-model.d.ts +29 -0
  493. package/dist/timeline-model.js +254 -0
  494. package/dist/timeline-model.js.map +1 -0
  495. package/dist/usage/allowance-copy.d.ts +25 -0
  496. package/dist/usage/session-usage-limit.d.ts +88 -0
  497. package/dist/usage/summary.d.ts +35 -0
  498. package/dist/usage.d.ts +17 -0
  499. package/dist/usage.js +1187 -0
  500. package/dist/usage.js.map +1 -0
  501. package/dist/voice-recording-owner.d.ts +7 -0
  502. package/package.json +106 -7
  503. package/src/approvals.ts +8 -1
  504. package/src/artifacts-spreadsheet.ts +1 -0
  505. package/src/artifacts.ts +44 -0
  506. package/src/assets/README.md +8 -0
  507. package/src/assets/gmail.ico +0 -0
  508. package/src/assets/outlook.ico +0 -0
  509. package/src/capability-catalog-row.tsx +94 -0
  510. package/src/capability-logo-fallback.ts +22 -0
  511. package/src/client.ts +20 -31
  512. package/src/commands/registry.ts +18 -15
  513. package/src/commands/types.ts +2 -0
  514. package/src/components/anthropic-mark.tsx +10 -0
  515. package/src/components/approval-surface.tsx +401 -138
  516. package/src/components/artifacts/artifact-chrome.tsx +413 -0
  517. package/src/components/artifacts/artifact-sandbox.tsx +193 -0
  518. package/src/components/artifacts/artifact-surface.tsx +40 -20
  519. package/src/components/artifacts/chat-interactive-block.tsx +260 -0
  520. package/src/components/artifacts/deferred-chat-media.tsx +67 -0
  521. package/src/components/artifacts/document-editor.tsx +141 -16
  522. package/src/components/artifacts/editable-artifact-ui.tsx +4 -2
  523. package/src/components/artifacts/editable-artifact-view.tsx +197 -0
  524. package/src/components/artifacts/editable-artifact-workbench.tsx +5 -2
  525. package/src/components/artifacts/editable-document.tsx +37 -36
  526. package/src/components/artifacts/editable-presentation.tsx +36 -18
  527. package/src/components/artifacts/editable-spreadsheet.tsx +543 -103
  528. package/src/components/artifacts/index.ts +45 -0
  529. package/src/components/artifacts/inline-html-document.ts +24 -0
  530. package/src/components/artifacts/latest-task-runner.ts +49 -0
  531. package/src/components/artifacts/presentation-editor.tsx +323 -67
  532. package/src/components/artifacts/published-html-artifact-frame.tsx +122 -5
  533. package/src/components/artifacts/session-artifact-viewer.tsx +378 -0
  534. package/src/components/artifacts/site-view.tsx +121 -0
  535. package/src/components/artifacts/spreadsheet-canvas.ts +5 -10
  536. package/src/components/artifacts/spreadsheet-command-queue.ts +184 -0
  537. package/src/components/artifacts/spreadsheet-download.tsx +109 -0
  538. package/src/components/artifacts/spreadsheet-general-display.ts +43 -0
  539. package/src/components/artifacts/spreadsheet-grid.tsx +344 -144
  540. package/src/components/artifacts/spreadsheet-resize.tsx +421 -0
  541. package/src/components/artifacts/spreadsheet-submissions.tsx +46 -0
  542. package/src/components/artifacts/visualization-assets/base.css +1015 -0
  543. package/src/components/artifacts/visualization-assets/helpers.html +349 -0
  544. package/src/components/artifacts/visualization-assets.gen.ts +3 -0
  545. package/src/components/browser-select-control.tsx +312 -0
  546. package/src/components/browser-viewer.tsx +778 -318
  547. package/src/components/chat-composer.tsx +47 -5
  548. package/src/components/child-session-link.tsx +36 -0
  549. package/src/components/claude-mark.tsx +11 -0
  550. package/src/components/code-editor.tsx +13 -16
  551. package/src/components/composer-transcription-control.tsx +128 -30
  552. package/src/components/composer.tsx +404 -195
  553. package/src/components/computer-viewer.tsx +880 -253
  554. package/src/components/copy-button.tsx +13 -5
  555. package/src/components/desktop-viewer.tsx +1 -1
  556. package/src/components/enrollment-consent.tsx +1 -1
  557. package/src/components/enrollment-device-flow.tsx +1 -1
  558. package/src/components/file-browser.tsx +10 -14
  559. package/src/components/fleet-tile.tsx +5 -3
  560. package/src/components/generated-video-player.tsx +4 -2
  561. package/src/components/grok-mark.tsx +34 -0
  562. package/src/components/human-input-form.tsx +590 -283
  563. package/src/components/human-input-surface.tsx +11 -0
  564. package/src/components/interaction-intervention-banner.tsx +2 -2
  565. package/src/components/linkified-text.tsx +79 -0
  566. package/src/components/machine-card.tsx +35 -78
  567. package/src/components/machine-dock-bar.tsx +1 -1
  568. package/src/components/machine-input-display.ts +197 -3
  569. package/src/components/machines/agent-status-icon.tsx +31 -0
  570. package/src/components/machines/machine-detail.tsx +200 -31
  571. package/src/components/machines/machine-operation-policy.tsx +1 -1
  572. package/src/components/machines/machine-status.ts +241 -0
  573. package/src/components/machines-dashboard.tsx +2 -17
  574. package/src/components/markdown-table-layout.ts +143 -0
  575. package/src/components/markdown.tsx +466 -129
  576. package/src/components/message-timeline.tsx +2370 -667
  577. package/src/components/model-mark.tsx +102 -0
  578. package/src/components/model-picker.tsx +15 -4
  579. package/src/components/model-policy-picker-menu.tsx +371 -0
  580. package/src/components/model-policy-picker.tsx +252 -429
  581. package/src/components/new-conversation.tsx +221 -0
  582. package/src/components/open-geni-chat.tsx +309 -0
  583. package/src/components/open-geni-links.tsx +179 -0
  584. package/src/components/openrouter-mark.tsx +13 -0
  585. package/src/components/opper-mark.tsx +18 -0
  586. package/src/components/pierre-diff.tsx +41 -16
  587. package/src/components/pierre-file.tsx +35 -10
  588. package/src/components/preview-loading.tsx +109 -0
  589. package/src/components/provider-recovery-notice.tsx +49 -0
  590. package/src/components/queue-item-content.ts +14 -0
  591. package/src/components/queue-surface-implementation.tsx +51 -356
  592. package/src/components/queue-surface-state.tsx +3 -1
  593. package/src/components/rehype-secret-refs.ts +98 -0
  594. package/src/components/remark-soft-line-breaks.ts +166 -0
  595. package/src/components/sandbox-files.tsx +91 -15
  596. package/src/components/sandbox-terminal.tsx +84 -13
  597. package/src/components/sandbox-workspace.tsx +147 -51
  598. package/src/components/secret-question-field.tsx +324 -0
  599. package/src/components/secret-ref-chip.tsx +185 -0
  600. package/src/components/session-capability-frame.tsx +197 -0
  601. package/src/components/session-chrome.tsx +842 -251
  602. package/src/components/session-commands-panel.tsx +126 -0
  603. package/src/components/session-commands.tsx +18 -0
  604. package/src/components/session-connection-request.tsx +97 -0
  605. package/src/components/session-conversation.tsx +876 -0
  606. package/src/components/session-list.tsx +257 -0
  607. package/src/components/session-mcp-capability-card.tsx +680 -0
  608. package/src/components/session-proxy-scope.tsx +153 -0
  609. package/src/components/session-status.tsx +26 -17
  610. package/src/components/session-usage-limit-notice.tsx +174 -0
  611. package/src/components/timeline-anchor.tsx +119 -0
  612. package/src/components/timeline-annotation-chrome.tsx +196 -0
  613. package/src/components/timeline-annotation-layout.ts +268 -0
  614. package/src/components/timeline-annotation-markers.tsx +185 -0
  615. package/src/components/timeline-annotation-reveal-context.ts +9 -0
  616. package/src/components/timeline-annotation-selection.tsx +106 -54
  617. package/src/components/timeline-annotation-shared.ts +245 -0
  618. package/src/components/timeline-annotations-dialog.tsx +185 -128
  619. package/src/components/timeline-annotations.tsx +189 -45
  620. package/src/components/timeline-reading-progress.ts +80 -0
  621. package/src/components/timeline-search.tsx +168 -0
  622. package/src/components/timeline-settlement.ts +60 -0
  623. package/src/components/tool-action-review.tsx +650 -0
  624. package/src/components/tool-review-history.tsx +88 -0
  625. package/src/components/usage-limit-notice.tsx +179 -0
  626. package/src/components/usage-member-list.tsx +792 -0
  627. package/src/components/usage-meter.tsx +382 -0
  628. package/src/components/use-viewer-menu-dismiss.ts +33 -0
  629. package/src/components/user-message-body.tsx +27 -2
  630. package/src/components/workspace-dock.tsx +18 -1
  631. package/src/composer-messages.ts +150 -0
  632. package/src/composer-voice-events.ts +12 -0
  633. package/src/composer.ts +6 -1
  634. package/src/connect-account-identity.tsx +34 -0
  635. package/src/connect-accounts.tsx +233 -0
  636. package/src/connect-chooser.tsx +336 -0
  637. package/src/connect-panel.tsx +99 -0
  638. package/src/connect-setup.tsx +355 -0
  639. package/src/connect.ts +60 -0
  640. package/src/connection-assets.d.ts +4 -0
  641. package/src/connection-catalog.tsx +162 -0
  642. package/src/connection-discovery.tsx +238 -0
  643. package/src/connection-installed.tsx +51 -0
  644. package/src/connection-logo.tsx +37 -0
  645. package/src/connection-service-logo.tsx +55 -0
  646. package/src/connection-service-presentation.ts +46 -0
  647. package/src/connection-skeleton.tsx +16 -0
  648. package/src/connection-type-picker.tsx +36 -0
  649. package/src/conversation-timeline.ts +92 -0
  650. package/src/desktop.ts +24 -0
  651. package/src/device-authorization.tsx +98 -0
  652. package/src/diffs.ts +16 -0
  653. package/src/discovery-cache.ts +42 -0
  654. package/src/editor.ts +13 -0
  655. package/src/hooks/internal.ts +3 -2
  656. package/src/hooks/latest-question.ts +164 -0
  657. package/src/hooks/shared-machines-list.ts +34 -1
  658. package/src/hooks/shared-query.ts +721 -0
  659. package/src/hooks/use-attached-browsers.ts +1 -1
  660. package/src/hooks/use-auth-runs.ts +0 -27
  661. package/src/hooks/use-available-models.ts +2 -2
  662. package/src/hooks/use-browser-frame-stream.ts +19 -2
  663. package/src/hooks/use-browser-session.ts +680 -205
  664. package/src/hooks/use-browser-sessions.ts +32 -1
  665. package/src/hooks/use-client-config-flags.ts +118 -0
  666. package/src/hooks/use-codex-accounts.ts +247 -47
  667. package/src/hooks/use-composer.ts +227 -44
  668. package/src/hooks/use-computer-frame-stream.ts +25 -7
  669. package/src/hooks/use-computer-input-posture.ts +94 -0
  670. package/src/hooks/use-computer-session.ts +409 -95
  671. package/src/hooks/use-connect.ts +23 -0
  672. package/src/hooks/use-desktop-stream.ts +19 -28
  673. package/src/hooks/use-file-attachments.ts +80 -4
  674. package/src/hooks/use-goal.ts +79 -9
  675. package/src/hooks/use-last-started-turn-policy.ts +37 -11
  676. package/src/hooks/use-machines.ts +4 -0
  677. package/src/hooks/use-new-conversation.ts +374 -0
  678. package/src/hooks/use-new-session-draft.ts +813 -0
  679. package/src/hooks/use-realtime-voice-models.ts +46 -0
  680. package/src/hooks/use-sandbox-files.ts +68 -20
  681. package/src/hooks/use-session-background-commands.ts +22 -11
  682. package/src/hooks/use-session-capabilities.ts +3 -2
  683. package/src/hooks/use-session-conversation.ts +150 -0
  684. package/src/hooks/use-session-events.ts +531 -335
  685. package/src/hooks/use-session-mcp-approval-policy.ts +13 -5
  686. package/src/hooks/use-session-model-account.ts +174 -0
  687. package/src/hooks/use-session.ts +30 -2
  688. package/src/hooks/use-slash-commands.ts +6 -11
  689. package/src/hooks/use-supergrok-accounts.ts +22 -8
  690. package/src/hooks/use-terminal-stream.ts +153 -27
  691. package/src/hooks/use-transcription.ts +6 -0
  692. package/src/hooks/use-turn-queue.ts +2 -1
  693. package/src/hooks/use-usage.ts +100 -0
  694. package/src/hooks/use-vaults.ts +153 -0
  695. package/src/hooks/use-video-artifact-playback.ts +1 -1
  696. package/src/hooks/use-voice-input.ts +127 -19
  697. package/src/hooks/use-workspace-capture.ts +3 -2
  698. package/src/hooks/use-workspace-edit.ts +23 -5
  699. package/src/hooks/use-workspace-sessions.ts +74 -7
  700. package/src/human-input-model.ts +359 -0
  701. package/src/identity-link-accounts.tsx +148 -0
  702. package/src/identity-link-consent.tsx +192 -0
  703. package/src/index.ts +262 -16
  704. package/src/interaction.ts +1 -0
  705. package/src/lib/clipboard.ts +29 -0
  706. package/src/lib/error-message.ts +37 -0
  707. package/src/lib/format.ts +205 -10
  708. package/src/lib/host-theme.ts +529 -0
  709. package/src/lib/interaction-errors.ts +15 -0
  710. package/src/lib/menu-styles.ts +44 -0
  711. package/src/lib/notify-observer.ts +15 -0
  712. package/src/lib/pierre-diffs-loader.ts +53 -0
  713. package/src/lib/provider-recovery.ts +243 -0
  714. package/src/lib/scrollbars.ts +30 -0
  715. package/src/lib/terminal-input-readiness.ts +51 -0
  716. package/src/lib/transient-retry.ts +99 -0
  717. package/src/lib/workbench-peers.ts +62 -0
  718. package/src/lib/xterm-theme.ts +23 -21
  719. package/src/mcp-connection-status.ts +35 -0
  720. package/src/model-mark-paths.ts +53 -0
  721. package/src/model-picker-model.ts +330 -0
  722. package/src/model-policy.ts +299 -74
  723. package/src/native-previews.ts +100 -0
  724. package/src/new-session-draft.ts +22 -0
  725. package/src/notification-text.ts +81 -0
  726. package/src/older-history.ts +22 -4
  727. package/src/organization-model.ts +118 -0
  728. package/src/plugin-details.tsx +185 -0
  729. package/src/plugin-discovery.tsx +196 -0
  730. package/src/preview-loading-paint.ts +68 -0
  731. package/src/provider.tsx +74 -15
  732. package/src/queue-presentation-model.ts +464 -0
  733. package/src/realtime/dropdown-menu.tsx +19 -12
  734. package/src/realtime/embedded-new-voice.tsx +1 -0
  735. package/src/realtime/embedded-voice.tsx +3 -0
  736. package/src/realtime/realtime-control.tsx +138 -550
  737. package/src/realtime/session-realtime.ts +566 -0
  738. package/src/sandbox-label-model.ts +53 -0
  739. package/src/scrollbars.ts +3 -0
  740. package/src/secret-reference-label.ts +43 -0
  741. package/src/secrets-ui.tsx +59 -0
  742. package/src/session-agents-model.ts +33 -0
  743. package/src/session-attention-model.ts +62 -0
  744. package/src/session-auth-recommendation.ts +48 -0
  745. package/src/session-capability-policy.ts +196 -0
  746. package/src/session-context.ts +3 -4
  747. package/src/session-feedback-model.ts +95 -0
  748. package/src/session-list-model.ts +434 -0
  749. package/src/session-realtime.ts +18 -0
  750. package/src/session-status-model.ts +99 -0
  751. package/src/session-ui.ts +98 -1
  752. package/src/session.ts +17 -1
  753. package/src/site-snapshot.ts +74 -0
  754. package/src/sites-ui.tsx +348 -0
  755. package/src/sites.ts +3 -0
  756. package/src/skill-discovery.tsx +208 -0
  757. package/src/terminal.ts +26 -0
  758. package/src/testing/session-scenarios.ts +404 -0
  759. package/src/timeline/activity-rail.tsx +242 -101
  760. package/src/timeline/agent-identity.tsx +84 -0
  761. package/src/timeline/agent-row.tsx +231 -0
  762. package/src/timeline/allowance-exhausted-row.tsx +86 -0
  763. package/src/timeline/compaction-copy.ts +115 -0
  764. package/src/timeline/fleet-decision-projection.ts +2 -1
  765. package/src/timeline/fleet-decision-row.tsx +1 -0
  766. package/src/timeline/focus-sequence.ts +44 -0
  767. package/src/timeline/genie-copy.ts +19 -0
  768. package/src/timeline/genie-loading.tsx +143 -0
  769. package/src/timeline/index.ts +30 -2
  770. package/src/timeline/knowledge-receipt.tsx +100 -0
  771. package/src/timeline/message-sender.tsx +71 -0
  772. package/src/timeline/notice-presentation.ts +48 -0
  773. package/src/timeline/parsers.ts +42 -2
  774. package/src/timeline/patch-apply-command.tsx +135 -0
  775. package/src/timeline/platform-activity-presentation.ts +166 -0
  776. package/src/timeline/platform-activity-row.tsx +15 -90
  777. package/src/timeline/presented-image.ts +63 -0
  778. package/src/timeline/projection.ts +1742 -76
  779. package/src/timeline/provider-logo.tsx +53 -0
  780. package/src/timeline/question-nav-model.ts +28 -0
  781. package/src/timeline/registry.ts +26 -4
  782. package/src/timeline/retained-image.ts +97 -0
  783. package/src/timeline/retained-loaders.ts +91 -0
  784. package/src/timeline/rolling-activity.tsx +80 -0
  785. package/src/timeline/screenshot-lightbox.tsx +228 -5
  786. package/src/timeline/session-usage-limit-row.tsx +42 -0
  787. package/src/timeline/shared.tsx +42 -15
  788. package/src/timeline/startup-dispatch-details.tsx +36 -0
  789. package/src/timeline/startup-preference.ts +33 -0
  790. package/src/timeline/startup-timings.tsx +145 -0
  791. package/src/timeline/tool-display-name.ts +19 -7
  792. package/src/timeline/tool-presentation.ts +1316 -0
  793. package/src/timeline/tool-renderers.tsx +835 -1138
  794. package/src/timeline/turn-summary-model.ts +343 -0
  795. package/src/timeline/turn-summary.tsx +272 -365
  796. package/src/timeline/types.ts +189 -6
  797. package/src/timeline/work-presentation.ts +189 -0
  798. package/src/timeline-model.ts +143 -0
  799. package/src/usage/allowance-copy.ts +64 -0
  800. package/src/usage/session-usage-limit.ts +266 -0
  801. package/src/usage/summary.ts +93 -0
  802. package/src/usage.ts +70 -0
  803. package/src/voice-recording-owner.ts +22 -0
  804. package/styles/compiled.css +2319 -748
  805. package/styles/connect.css +290 -0
  806. package/styles/connect.d.ts +1 -0
  807. package/styles/effective-tokens.css +1 -0
  808. package/styles/index.css +309 -9
  809. package/styles/responsive.css +99 -7
  810. package/styles/session-capability.css +44 -0
  811. package/styles/tokens.css +134 -66
  812. package/dist/browser-viewer-MKKFRRRV.js +0 -14
  813. package/dist/chunk-2DZVYHVO.js +0 -470
  814. package/dist/chunk-2DZVYHVO.js.map +0 -1
  815. package/dist/chunk-3AGBGTRT.js.map +0 -1
  816. package/dist/chunk-4MDLEHXL.js.map +0 -1
  817. package/dist/chunk-5YK2ZDIG.js.map +0 -1
  818. package/dist/chunk-7EAFGICR.js +0 -302
  819. package/dist/chunk-7EAFGICR.js.map +0 -1
  820. package/dist/chunk-ELIYWBZR.js.map +0 -1
  821. package/dist/chunk-H5YGYNXS.js.map +0 -1
  822. package/dist/chunk-HUIKIDM5.js.map +0 -1
  823. package/dist/chunk-ICA6R6TW.js.map +0 -1
  824. package/dist/chunk-IOLVN74D.js.map +0 -1
  825. package/dist/chunk-JAQ5K7W6.js.map +0 -1
  826. package/dist/chunk-JVYQRRC4.js.map +0 -1
  827. package/dist/chunk-KCGXPP5V.js.map +0 -1
  828. package/dist/chunk-KZ73RL76.js.map +0 -1
  829. package/dist/chunk-NEXLLTP4.js.map +0 -1
  830. package/dist/chunk-PEI6RANI.js.map +0 -1
  831. package/dist/chunk-VTOREEXF.js +0 -142
  832. package/dist/chunk-VTOREEXF.js.map +0 -1
  833. package/dist/chunk-WSK7G5LE.js +0 -170
  834. package/dist/chunk-WSK7G5LE.js.map +0 -1
  835. package/dist/chunk-X4A66EWZ.js +0 -223
  836. package/dist/chunk-X4A66EWZ.js.map +0 -1
  837. package/dist/chunk-XYRDDSXF.js.map +0 -1
  838. package/dist/chunk-YG4QEQF5.js +0 -9557
  839. package/dist/chunk-YG4QEQF5.js.map +0 -1
  840. package/dist/chunk-YVCHAQZY.js.map +0 -1
  841. package/dist/chunk-ZPBTI3TE.js.map +0 -1
  842. package/dist/computer-viewer-FWEZISW4.js +0 -16
  843. package/dist/fleet-decision-row-GGCAT4E4.js.map +0 -1
  844. package/dist/hooks/use-environments.d.ts +0 -41
  845. package/dist/hooks/use-packs.d.ts +0 -32
  846. package/dist/platform-activity-row-BJHUHXAT.js.map +0 -1
  847. package/dist/queue-surface-implementation-BSKUWHRI.js.map +0 -1
  848. package/dist/timeline-annotation-selection-3L7LHGG2.js +0 -187
  849. package/dist/timeline-annotation-selection-3L7LHGG2.js.map +0 -1
  850. package/dist/timeline-annotations-dialog-U7EVLR75.js +0 -196
  851. package/dist/timeline-annotations-dialog-U7EVLR75.js.map +0 -1
  852. package/src/hooks/use-environments.ts +0 -182
  853. package/src/hooks/use-packs.ts +0 -157
  854. /package/dist/{browser-viewer-MKKFRRRV.js.map → browser-viewer-ZWR6MWJ7.js.map} +0 -0
  855. /package/dist/{computer-viewer-FWEZISW4.js.map → chat-interactive-block-4EUKJXUO.js.map} +0 -0
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  ComputerAction,
3
+ ComputerActionReceipt,
3
4
  ComputerClipboard,
4
5
  ComputerFrame,
5
6
  ComputerObservation,
@@ -25,6 +26,7 @@ import {
25
26
  } from "lucide-react";
26
27
  import {
27
28
  type ClipboardEvent,
29
+ type CompositionEvent,
28
30
  type KeyboardEvent,
29
31
  type MouseEvent,
30
32
  type PointerEvent,
@@ -32,6 +34,7 @@ import {
32
34
  type WheelEvent,
33
35
  useCallback,
34
36
  useEffect,
37
+ useLayoutEffect,
35
38
  useMemo,
36
39
  useRef,
37
40
  useState,
@@ -40,7 +43,12 @@ import {
40
43
  type ComputerFrameWebSocketFactory,
41
44
  useComputerFrameStream,
42
45
  } from "../hooks/use-computer-frame-stream";
43
- import { useComputerSession } from "../hooks/use-computer-session";
46
+ import {
47
+ computerActionReceiptError,
48
+ isStaleComputerActionResult,
49
+ useComputerSession,
50
+ } from "../hooks/use-computer-session";
51
+ import { useComputerInputPosture } from "../hooks/use-computer-input-posture";
44
52
  import { useComputerSessions } from "../hooks/use-computer-sessions";
45
53
  import { useInteractionInterventions } from "../hooks/use-interaction-interventions";
46
54
  import { cn } from "../lib/cn";
@@ -49,11 +57,12 @@ import { isSourcePlacementChangedError } from "../lib/interaction-errors";
49
57
  import type { EmbeddedComputerInteractionClientOverride } from "../session-context";
50
58
  import { InteractionInterventionBanner } from "./interaction-intervention-banner";
51
59
  import { DesktopViewer } from "./desktop-viewer";
60
+ import { useViewerMenuDismiss } from "./use-viewer-menu-dismiss";
52
61
 
53
62
  export type ComputerViewerNotification = { kind: "error" | "info"; message: string };
54
63
 
55
64
  export type ComputerViewerProps = EmbeddedComputerInteractionClientOverride & {
56
- /** Selected OpenGeni agent/session. Peer ComputerSessions remain visible. */
65
+ /** Selected Opengeni agent/session. Peer ComputerSessions remain visible. */
57
66
  sessionId: string;
58
67
  enabled?: boolean | undefined;
59
68
  className?: string | undefined;
@@ -170,6 +179,7 @@ export function ComputerViewer({
170
179
 
171
180
  const notifyError = useCallback(
172
181
  (cause: unknown, fallback: string) => {
182
+ if (isStaleComputerActionResult(cause)) return;
173
183
  onNotify?.({
174
184
  kind: "error",
175
185
  message: cause instanceof Error ? cause.message : fallback,
@@ -253,6 +263,9 @@ export function ComputerViewer({
253
263
  computerSessionId: selection?.sessionId ?? null,
254
264
  enabled: enabled && selection !== null && controllerReady,
255
265
  });
266
+ useEffect(() => {
267
+ setShowControls(computer.selectedTarget?.kind === "app");
268
+ }, [sessionId, selection?.sessionId, computer.selectedTarget?.id, computer.selectedTarget?.kind]);
256
269
  useEffect(() => {
257
270
  if (!isSourcePlacementChangedError(computer.error, "computer_session")) return;
258
271
  void refreshRegistry();
@@ -282,18 +295,69 @@ export function ComputerViewer({
282
295
  const displayedFrame =
283
296
  frames.frame &&
284
297
  frames.frame.computerSessionId === selection?.sessionId &&
285
- frames.frame.targetId === computer.selectedTarget?.id
298
+ frames.frame.targetId === computer.selectedTarget?.id &&
299
+ frames.frame.targetGeneration === computer.selectedTarget?.targetGeneration
286
300
  ? frames.frame
287
301
  : null;
288
302
  const rfbStream =
289
303
  frames.attachment?.stream.kind === "direct_rfb" ? frames.attachment.stream : null;
304
+ const appTarget = computer.selectedTarget?.kind === "app";
305
+ const appInputPosture = useComputerInputPosture({
306
+ ...override,
307
+ computerSessionId: selection?.sessionId ?? null,
308
+ controllerGeneration: computer.session?.controller?.controllerGeneration ?? null,
309
+ enabled: enabled && controllerReady && appTarget,
310
+ });
311
+ const inputPostureRef = useRef({
312
+ sessionId: selection?.sessionId ?? null,
313
+ controllerGeneration: computer.session?.controller?.controllerGeneration ?? null,
314
+ denied: false,
315
+ });
316
+ const postureSessionId = selection?.sessionId ?? null;
317
+ const postureController = computer.session?.controller?.controllerGeneration ?? null;
318
+ if (
319
+ inputPostureRef.current.sessionId !== postureSessionId ||
320
+ inputPostureRef.current.controllerGeneration !== postureController
321
+ ) {
322
+ inputPostureRef.current = {
323
+ sessionId: postureSessionId,
324
+ controllerGeneration: postureController,
325
+ denied: false,
326
+ };
327
+ }
328
+ if (
329
+ frames.attachment?.computerSessionId === postureSessionId &&
330
+ frames.attachment.controllerGeneration === postureController
331
+ ) {
332
+ inputPostureRef.current.denied = frames.attachment.inputAllowed === false;
333
+ }
334
+ // Keep a known denial while a fresh attachment is pending or a semantic-only
335
+ // target has no stream. Only a fresh posture for this controller clears it.
336
+ const inputDenied = appTarget
337
+ ? appInputPosture.inputAllowed !== true
338
+ : inputPostureRef.current.denied;
339
+ const attachmentInputAllowed = frames.attachment !== null && !inputDenied;
340
+ const inputPosturePending = appTarget
341
+ ? appInputPosture.pending
342
+ : inputDenied && (frames.state === "attaching" || frames.state === "reconnecting");
343
+ const machineLocked = selectedRegistrySession?.failureCode === "machine_locked";
344
+ const controlUnavailable = computer.controlError !== null;
345
+ const connectionState = controlUnavailable ? "error" : frames.state;
346
+ // RFB has one combined input switch; partial native input stays view-only.
347
+ const rfbInputEnabled =
348
+ rfbStream?.inputAllowed === true &&
349
+ attachmentInputAllowed &&
350
+ !machineLocked &&
351
+ !controlUnavailable &&
352
+ computer.session?.capabilities?.pointerInput === true &&
353
+ computer.session?.capabilities?.keyboardInput === true;
290
354
  const rfbCapability = useMemo<DesktopStreamCapability | null>(() => {
291
355
  if (!rfbStream || !frames.attachment) return null;
292
356
  const bounds = computer.selectedTarget?.bounds;
293
357
  return {
294
358
  transport: "vnc-ws",
295
359
  client: "novnc",
296
- mode: "interactive",
360
+ mode: rfbInputEnabled ? "interactive" : "read-only",
297
361
  url: rfbStream.url,
298
362
  token: null,
299
363
  expiresAt: frames.attachment.expiresAt,
@@ -305,8 +369,7 @@ export function ComputerViewer({
305
369
  sharedSessionIds: [],
306
370
  reason: null,
307
371
  };
308
- }, [computer.selectedTarget?.bounds, frames.attachment, rfbStream]);
309
- const machineLocked = selectedRegistrySession?.failureCode === "machine_locked";
372
+ }, [computer.selectedTarget?.bounds, frames.attachment, rfbInputEnabled, rfbStream]);
310
373
  const currentIds = useMemo(() => new Set(relevant.map((session) => session.id)), [relevant]);
311
374
 
312
375
  useEffect(() => {
@@ -433,37 +496,57 @@ export function ComputerViewer({
433
496
  ]);
434
497
 
435
498
  const perform = useCallback(
436
- async (action: ComputerAction, frame: ComputerFrame | null): Promise<void> => {
499
+ async (
500
+ action: ComputerAction,
501
+ frame: ComputerFrame | null,
502
+ operationId?: string,
503
+ ): Promise<ComputerActionReceipt> => {
504
+ if (computer.controlError) throw computer.controlError;
505
+ if (inputDenied) throw new Error("Desktop is view only. Refresh to check desktop controls.");
506
+ let receipt;
437
507
  if (action.type === "pointer") {
438
508
  if (!frame) throw new Error("Desktop view is not ready for pointer input.");
439
- await actFromFrame(action, frame);
509
+ receipt = await actFromFrame(action, frame, operationId);
440
510
  } else {
441
- await act(action);
511
+ receipt = await act(action, operationId);
442
512
  }
513
+ if (!isStaleComputerActionResult(receipt) && receipt.state !== "completed") {
514
+ throw computerActionReceiptError(receipt);
515
+ }
516
+ return receipt;
443
517
  },
444
- [act, actFromFrame],
518
+ [act, actFromFrame, computer.controlError, inputDenied],
445
519
  );
446
520
 
521
+ const reconnect = () => {
522
+ void refreshComputer();
523
+ if (appTarget) appInputPosture.refresh();
524
+ else frames.reconnect();
525
+ };
526
+
447
527
  const copyFromRfb = useCallback(
448
528
  (event: ClipboardEvent<HTMLDivElement>) => {
449
- if (!rfbStream || computer.session?.capabilities?.clipboard !== true) return;
529
+ if (!rfbInputEnabled || !rfbStream || computer.session?.capabilities?.clipboard !== true)
530
+ return;
450
531
  event.preventDefault();
451
532
  event.stopPropagation();
452
- void perform({ type: "clipboard", operation: "copy" }, null)
453
- .then(() => computer.readClipboard())
454
- .then(async (clipboard) => {
455
- if (clipboard.text && !(await copyTextToClipboard(clipboard.text))) {
456
- throw new Error("Desktop text could not be copied to the local clipboard");
457
- }
458
- })
459
- .catch((cause) => notifyError(cause, "Could not copy from the desktop."));
533
+ void (async () => {
534
+ const receipt = await perform({ type: "clipboard", operation: "copy" }, null);
535
+ if (isStaleComputerActionResult(receipt)) return;
536
+ const clipboard = await computer.readClipboard();
537
+ if (isStaleComputerActionResult(receipt) || isStaleComputerActionResult(clipboard)) return;
538
+ if (clipboard.text && !(await copyTextToClipboard(clipboard.text))) {
539
+ throw new Error("Desktop text could not be copied to the local clipboard");
540
+ }
541
+ })().catch((cause) => notifyError(cause, "Could not copy from the desktop."));
460
542
  },
461
- [computer, notifyError, perform, rfbStream],
543
+ [computer, notifyError, perform, rfbInputEnabled, rfbStream],
462
544
  );
463
545
 
464
546
  const pasteIntoRfb = useCallback(
465
547
  (text: string): boolean => {
466
- if (!rfbStream || computer.session?.capabilities?.clipboard !== true) return false;
548
+ if (!rfbInputEnabled || !rfbStream || computer.session?.capabilities?.clipboard !== true)
549
+ return false;
467
550
  // Keep paste on the canonical ComputerSession action path. RFB
468
551
  // ClientCutText synchronization varies by server and can acknowledge a
469
552
  // local paste without ever updating the remote graphical seat. These two
@@ -472,13 +555,16 @@ export function ComputerViewer({
472
555
  // ownership is asynchronous, so a successful write response alone does
473
556
  // not prove that the graphical seat can already serve the selection.
474
557
  void (async () => {
475
- await perform({ type: "clipboard", operation: "write", text }, null);
476
- assertExactComputerClipboard(await computer.readClipboard(), text);
558
+ const receipt = await perform({ type: "clipboard", operation: "write", text }, null);
559
+ if (isStaleComputerActionResult(receipt)) return;
560
+ const clipboard = await computer.readClipboard();
561
+ if (isStaleComputerActionResult(receipt) || isStaleComputerActionResult(clipboard)) return;
562
+ assertExactComputerClipboard(clipboard, text);
477
563
  await perform({ type: "clipboard", operation: "paste" }, null);
478
564
  })().catch((cause) => notifyError(cause, "Could not paste into the desktop."));
479
565
  return true;
480
566
  },
481
- [computer, notifyError, perform, rfbStream],
567
+ [computer, notifyError, perform, rfbInputEnabled, rfbStream],
482
568
  );
483
569
 
484
570
  const hideGenericCreate = suppressGenericCreate && liveRelevant.length === 0;
@@ -501,11 +587,11 @@ export function ComputerViewer({
501
587
  return (
502
588
  <div className={cn("grid h-full place-items-center bg-og-bg p-6", className)}>
503
589
  <div className="max-w-sm text-center">
504
- <CircleAlertIcon className="mx-auto size-5 text-og-status-error" />
590
+ <CircleAlertIcon className="mx-auto size-5 text-og-danger" />
505
591
  <p className="mt-3 text-og-menu font-medium text-og-fg">
506
592
  Chrome reconnected—open a fresh browser/desktop.
507
593
  </p>
508
- <p className="mt-1 text-og-control leading-5 text-og-muted">
594
+ <p className="mt-1 text-og-control leading-5 text-og-fg-muted">
509
595
  This Chrome profile is live again, but the previous desktop cannot move to the new
510
596
  connection. Use Browser → New browser → Connected Chrome.
511
597
  </p>
@@ -518,9 +604,9 @@ export function ComputerViewer({
518
604
  return (
519
605
  <div className={cn("grid h-full place-items-center bg-og-bg p-6", className)}>
520
606
  <div className="max-w-sm text-center">
521
- <span className="mx-auto grid size-10 place-items-center rounded-og-lg border border-og-border bg-og-surface-1 text-og-muted">
607
+ <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">
522
608
  {error ? (
523
- <CircleAlertIcon className="size-5 text-og-status-error" />
609
+ <CircleAlertIcon className="size-5 text-og-danger" />
524
610
  ) : (
525
611
  <LoaderCircleIcon className="size-5 animate-spin" />
526
612
  )}
@@ -528,7 +614,7 @@ export function ComputerViewer({
528
614
  <p className="mt-3 text-og-menu font-medium text-og-fg">
529
615
  {error ? "Desktop didn’t open" : "Opening desktop…"}
530
616
  </p>
531
- <p className="mt-1 text-og-control leading-5 text-og-muted">
617
+ <p className="mt-1 text-og-control leading-5 text-og-fg-muted">
532
618
  {error?.message ?? "Preparing this agent’s desktop. It will appear here when ready."}
533
619
  </p>
534
620
  {error ? (
@@ -536,7 +622,7 @@ export function ComputerViewer({
536
622
  type="button"
537
623
  onClick={createError ? createComputer : () => void refreshRegistry()}
538
624
  disabled={creating || registry.refreshing}
539
- 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"
625
+ 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"
540
626
  >
541
627
  {creating || registry.refreshing ? (
542
628
  <LoaderCircleIcon className="size-3.5 animate-spin" />
@@ -552,14 +638,27 @@ export function ComputerViewer({
552
638
  }
553
639
 
554
640
  return (
555
- <div className={cn("flex h-full min-h-0 flex-col overflow-hidden bg-og-bg", className)}>
641
+ <div
642
+ className={cn(
643
+ "@container/computer-viewer relative flex h-full min-h-0 min-w-0 flex-col overflow-hidden bg-og-bg",
644
+ className,
645
+ )}
646
+ >
556
647
  <ComputerToolbar
557
648
  sessions={liveSessions}
558
649
  relevantSessionIds={currentIds}
559
650
  selectedSessionId={selection?.sessionId ?? null}
560
651
  creating={creating}
561
- refreshing={registry.refreshing}
652
+ refreshing={registry.refreshing || inputPosturePending}
562
653
  interventionCounts={interventionCounts}
654
+ targets={controllerReady ? computer.targets : []}
655
+ selectedTargetId={computer.selectedTarget?.id ?? null}
656
+ targetsLoading={controllerReady && computer.loading}
657
+ onSelectTarget={(targetId) => {
658
+ void computer
659
+ .selectTarget(targetId)
660
+ .catch((cause) => notifyError(cause, "Could not switch desktop views."));
661
+ }}
563
662
  onSelect={(computerSessionId) =>
564
663
  selectComputerSession({ sessionId: computerSessionId, pinned: true })
565
664
  }
@@ -567,7 +666,18 @@ export function ComputerViewer({
567
666
  if (relevant[0]) selectComputerSession({ sessionId: relevant[0].id, pinned: false });
568
667
  }}
569
668
  onCreate={hideGenericCreate ? undefined : createComputer}
570
- onRefresh={() => void Promise.all([refreshRegistry(), refreshComputer()])}
669
+ onRefresh={() => {
670
+ if (appTarget) {
671
+ appInputPosture.refresh();
672
+ } else if (
673
+ (connectionState === "error" || inputDenied) &&
674
+ !generationLossSelected &&
675
+ !generationLossFrames
676
+ ) {
677
+ frames.reconnect();
678
+ }
679
+ void Promise.all([refreshRegistry(), refreshComputer()]);
680
+ }}
571
681
  />
572
682
  <InteractionInterventionBanner
573
683
  interventions={selectedInterventions}
@@ -597,46 +707,81 @@ export function ComputerViewer({
597
707
  />
598
708
  ) : (
599
709
  <>
600
- <ComputerTargetRail
601
- targets={computer.targets}
602
- selectedTargetId={computer.selectedTarget?.id ?? null}
603
- loading={computer.loading}
604
- onSelect={(targetId) =>
605
- void computer
606
- .selectTarget(targetId)
607
- .catch((cause) => notifyError(cause, "Could not switch desktop views."))
608
- }
609
- />
610
- <div className="flex min-h-0 flex-1">
710
+ <div className="flex min-h-0 flex-1 flex-col @xl/computer-viewer:flex-row">
611
711
  {rfbStream ? (
612
712
  <div className="relative min-h-0 flex-1 bg-black" onCopyCapture={copyFromRfb}>
613
713
  <DesktopViewer
614
714
  capability={rfbCapability}
615
- interactive
715
+ interactive={rfbInputEnabled}
616
716
  showControlToggle={false}
617
717
  webSocketProtocols={rfbStream.protocols}
618
718
  onPasteText={pasteIntoRfb}
619
719
  targetPlatform={computer.session?.platform ?? null}
620
720
  className="h-full"
621
721
  />
722
+ {!rfbInputEnabled ? (
723
+ <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">
724
+ {controlUnavailable ? (
725
+ <>
726
+ Desktop controls unavailable{" "}
727
+ <button
728
+ type="button"
729
+ onClick={reconnect}
730
+ className="pointer-events-auto ml-2 font-medium underline"
731
+ >
732
+ Reconnect
733
+ </button>
734
+ </>
735
+ ) : (
736
+ "View only · direct input unavailable"
737
+ )}
738
+ </div>
739
+ ) : null}
622
740
  </div>
623
741
  ) : (
624
742
  <ComputerViewport
743
+ key={JSON.stringify([
744
+ selection?.sessionId,
745
+ computer.selectedTarget?.id,
746
+ computer.selectedTarget?.targetGeneration,
747
+ frames.attachment?.controllerGeneration,
748
+ ])}
625
749
  frame={machineLocked ? null : displayedFrame}
626
750
  observation={computer.observation}
627
751
  target={computer.selectedTarget}
628
752
  machineLocked={machineLocked}
629
- connectionState={frames.state}
630
- connectionError={frames.error ?? computer.error}
753
+ controlUnavailable={controlUnavailable}
754
+ inputAllowed={!inputDenied}
755
+ connectionState={connectionState}
756
+ connectionError={
757
+ computer.controlError ?? appInputPosture.error ?? frames.error ?? computer.error
758
+ }
631
759
  mutating={computer.mutating}
632
760
  backgroundActions={computer.session?.capabilities?.backgroundActions === true}
633
- clipboardEnabled={computer.session?.capabilities?.clipboard === true}
761
+ backgroundInput={computer.session?.capabilities?.backgroundInput === true}
762
+ allowForegroundInputWithinOwnedSeat={
763
+ computer.session?.platform === "linux" &&
764
+ computer.session.adapter === "opengeni.cua.linux.v1" &&
765
+ computer.session.seatId === `linux-virtual:${computer.session.id}` &&
766
+ Boolean(computer.session.displayId)
767
+ }
768
+ clipboardEnabled={
769
+ attachmentInputAllowed && computer.session?.capabilities?.clipboard === true
770
+ }
771
+ pointerInput={
772
+ attachmentInputAllowed && computer.session?.capabilities?.pointerInput === true
773
+ }
774
+ pointerClickContinuation={
775
+ attachmentInputAllowed &&
776
+ computer.session?.capabilities?.pointerClickContinuation === true
777
+ }
778
+ keyboardInput={
779
+ attachmentInputAllowed && computer.session?.capabilities?.keyboardInput === true
780
+ }
634
781
  onAction={perform}
635
782
  onReadClipboard={computer.readClipboard}
636
783
  onReconnect={
637
- generationLossSelected || generationLossFrames
638
- ? () => undefined
639
- : frames.reconnect
784
+ generationLossSelected || generationLossFrames ? () => undefined : reconnect
640
785
  }
641
786
  onError={(cause) => notifyError(cause, "Desktop input failed.")}
642
787
  />
@@ -644,7 +789,9 @@ export function ComputerViewer({
644
789
  {showControls ? (
645
790
  <ComputerSemanticPanel
646
791
  observation={computer.observation}
647
- mutating={computer.mutating}
792
+ mutating={computer.mutating || controlUnavailable || inputDenied}
793
+ controlUnavailable={controlUnavailable}
794
+ inputAllowed={!inputDenied}
648
795
  onAction={(action) =>
649
796
  void perform(action, null).catch((cause) =>
650
797
  notifyError(cause, "Desktop action failed."),
@@ -656,10 +803,12 @@ export function ComputerViewer({
656
803
  <ComputerStatusBar
657
804
  session={computer.session}
658
805
  target={computer.selectedTarget}
659
- connectionState={frames.state}
806
+ connectionState={connectionState}
807
+ controlUnavailable={controlUnavailable}
808
+ inputAllowed={!inputDenied}
660
809
  refreshing={registry.refreshing}
661
810
  showControls={showControls}
662
- controlCount={semanticNodes(computer.observation).length}
811
+ controlCount={semanticControls(computer.observation).length}
663
812
  onToggleControls={() => setShowControls((current) => !current)}
664
813
  />
665
814
  </>
@@ -678,9 +827,9 @@ function ComputerUnselectedPanel(props: {
678
827
  return (
679
828
  <div className="grid min-h-0 flex-1 place-items-center bg-og-bg p-6 text-center">
680
829
  <div className="max-w-sm">
681
- <span className="mx-auto grid size-10 place-items-center rounded-og-md border border-og-border bg-og-surface-1 text-og-muted">
830
+ <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">
682
831
  {props.error || props.generationLoss ? (
683
- <CircleAlertIcon className="size-4.5 text-og-status-error" />
832
+ <CircleAlertIcon className="size-4.5 text-og-danger" />
684
833
  ) : (
685
834
  <LoaderCircleIcon className="size-4.5 animate-spin" />
686
835
  )}
@@ -692,7 +841,7 @@ function ComputerUnselectedPanel(props: {
692
841
  ? "Desktop didn’t open"
693
842
  : "Opening desktop…"}
694
843
  </p>
695
- <p className="mt-1 text-og-control leading-5 text-og-muted">
844
+ <p className="mt-1 text-og-control leading-5 text-og-fg-muted">
696
845
  {props.generationLoss
697
846
  ? "This Chrome profile is live again, but the previous desktop cannot move to the new connection. Use Browser → New browser → Connected Chrome."
698
847
  : (props.error?.message ??
@@ -727,66 +876,97 @@ function ComputerToolbar(props: {
727
876
  creating: boolean;
728
877
  refreshing: boolean;
729
878
  interventionCounts: Map<string, number>;
879
+ targets: ComputerTarget[];
880
+ selectedTargetId: string | null;
881
+ targetsLoading: boolean;
882
+ onSelectTarget: (id: string) => void;
730
883
  onSelect: (id: string) => void;
731
884
  onFollow: () => void;
732
885
  onCreate?: (() => void) | undefined;
733
886
  onRefresh: () => void;
734
887
  }) {
735
- const detailsRef = useRef<HTMLDetailsElement | null>(null);
888
+ const detailsRef = useViewerMenuDismiss();
736
889
  const selected = props.sessions.find((session) => session.id === props.selectedSessionId);
737
890
  const current = props.sessions.filter((session) => props.relevantSessionIds.has(session.id));
738
891
  const others = props.sessions.filter((session) => !props.relevantSessionIds.has(session.id));
739
- const choose = (id: string) => {
740
- props.onSelect(id);
892
+ const closeMenu = (restoreFocus: boolean) => {
741
893
  detailsRef.current?.removeAttribute("open");
894
+ if (restoreFocus) detailsRef.current?.querySelector("summary")?.focus();
895
+ };
896
+ const choose = (id: string, restoreFocus: boolean) => {
897
+ props.onSelect(id);
898
+ closeMenu(restoreFocus);
742
899
  };
743
900
  return (
744
- <div className="flex h-10 shrink-0 items-center gap-1.5 border-b border-og-border bg-og-surface-0 px-2">
745
- <details ref={detailsRef} className="relative min-w-0">
746
- <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">
747
- <MonitorIcon className="size-3.5 shrink-0 text-og-muted" />
901
+ <div className="flex h-10 shrink-0 items-center gap-1.5 border-b border-og-border bg-og-surface-1 px-2">
902
+ <details ref={detailsRef} className="min-w-0">
903
+ <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">
904
+ <MonitorIcon className="size-3.5 shrink-0 text-og-fg-muted" />
748
905
  <span className="truncate font-medium">
749
906
  {selected?.name === "Computer" ? "Desktop" : (selected?.name ?? "Desktop")}
750
907
  </span>
751
908
  {(props.interventionCounts.get(selected?.id ?? "") ?? 0) > 0 ? (
752
909
  <span className="size-1.5 shrink-0 rounded-full bg-og-status-waiting" />
753
910
  ) : null}
754
- <ChevronDownIcon className="size-3 shrink-0 text-og-subtle" />
911
+ <ChevronDownIcon className="size-3 shrink-0 text-og-fg-subtle" />
755
912
  </summary>
756
- <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">
757
- <ComputerSessionGroup
758
- label="Current agent"
759
- sessions={current}
760
- selectedId={props.selectedSessionId}
761
- interventionCounts={props.interventionCounts}
762
- onSelect={choose}
763
- />
764
- <ComputerSessionGroup
765
- label="Other agents"
766
- sessions={others}
767
- selectedId={props.selectedSessionId}
768
- interventionCounts={props.interventionCounts}
769
- onSelect={choose}
770
- />
771
- <div className="mt-1 flex gap-1 border-t border-og-border pt-1">
913
+ <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">
914
+ <div className="min-h-0 max-h-96 overflow-y-auto overscroll-contain">
915
+ <ComputerSessionGroup
916
+ label="Current agent"
917
+ sessions={current}
918
+ selectedId={props.selectedSessionId}
919
+ interventionCounts={props.interventionCounts}
920
+ onSelect={choose}
921
+ />
922
+ <ComputerSessionGroup
923
+ label="Other agents"
924
+ sessions={others}
925
+ selectedId={props.selectedSessionId}
926
+ interventionCounts={props.interventionCounts}
927
+ onSelect={choose}
928
+ />
929
+ </div>
930
+ <div className="mt-1 flex shrink-0 gap-1 border-t border-og-border pt-1">
772
931
  {current.length > 0 ? (
773
- <MenuButton onClick={props.onFollow}>Follow agent</MenuButton>
932
+ <MenuButton
933
+ onClick={(event) => {
934
+ props.onFollow();
935
+ closeMenu(event.detail === 0);
936
+ }}
937
+ >
938
+ Follow agent
939
+ </MenuButton>
774
940
  ) : null}
775
941
  {props.onCreate ? (
776
- <MenuButton onClick={props.onCreate} disabled={props.creating}>
942
+ <MenuButton
943
+ onClick={(event) => {
944
+ props.onCreate?.();
945
+ closeMenu(event.detail === 0);
946
+ }}
947
+ disabled={props.creating}
948
+ >
777
949
  <PlusIcon className="size-3.5" /> New desktop
778
950
  </MenuButton>
779
951
  ) : null}
780
952
  </div>
781
953
  </div>
782
954
  </details>
783
- <span className="min-w-0 flex-1 truncate text-og-xs text-og-subtle">
955
+ <span className="min-w-0 flex-1 truncate text-og-xs text-og-fg-subtle">
784
956
  {selected ? `${platformLabel(selected)} · ${placementLabel(selected)}` : ""}
785
957
  </span>
958
+ <ComputerTargetPicker
959
+ targets={props.targets}
960
+ selectedTargetId={props.selectedTargetId}
961
+ loading={props.targetsLoading}
962
+ onSelect={props.onSelectTarget}
963
+ />
786
964
  <button
787
965
  type="button"
788
966
  onClick={props.onRefresh}
789
- className="grid size-7 place-items-center rounded-og-sm text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg"
967
+ disabled={props.refreshing}
968
+ aria-busy={props.refreshing}
969
+ className="grid size-7 place-items-center rounded-og-sm text-og-fg-muted transition hover:bg-og-surface-2 hover:text-og-fg"
790
970
  aria-label="Refresh desktops"
791
971
  >
792
972
  <RefreshCwIcon className={cn("size-3.5", props.refreshing && "animate-spin")} />
@@ -796,7 +976,7 @@ function ComputerToolbar(props: {
796
976
  type="button"
797
977
  onClick={props.onCreate}
798
978
  disabled={props.creating}
799
- 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"
979
+ 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"
800
980
  aria-label="Open a new desktop"
801
981
  >
802
982
  {props.creating ? (
@@ -815,12 +995,12 @@ function ComputerSessionGroup(props: {
815
995
  sessions: ComputerSession[];
816
996
  selectedId: string | null;
817
997
  interventionCounts: Map<string, number>;
818
- onSelect: (id: string) => void;
998
+ onSelect: (id: string, restoreFocus: boolean) => void;
819
999
  }) {
820
1000
  if (props.sessions.length === 0) return null;
821
1001
  return (
822
1002
  <div className="py-1">
823
- <p className="px-2 pb-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-subtle">
1003
+ <p className="px-2 pb-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-fg-subtle">
824
1004
  {props.label}
825
1005
  </p>
826
1006
  {props.sessions.map((session) => {
@@ -829,7 +1009,8 @@ function ComputerSessionGroup(props: {
829
1009
  <button
830
1010
  key={session.id}
831
1011
  type="button"
832
- onClick={() => props.onSelect(session.id)}
1012
+ onClick={(event) => props.onSelect(session.id, event.detail === 0)}
1013
+ aria-pressed={session.id === props.selectedId}
833
1014
  className={cn(
834
1015
  "flex w-full items-center gap-2 rounded-og-sm px-2 py-1.5 text-left transition hover:bg-og-surface-2",
835
1016
  session.id === props.selectedId && "bg-og-surface-2",
@@ -843,7 +1024,7 @@ function ComputerSessionGroup(props: {
843
1024
  />
844
1025
  <span className="min-w-0 flex-1">
845
1026
  <span className="block truncate text-og-control text-og-fg">{session.name}</span>
846
- <span className="block truncate text-og-xs text-og-subtle">
1027
+ <span className="block truncate text-og-xs text-og-fg-subtle">
847
1028
  {platformLabel(session)} · {placementLabel(session)}
848
1029
  </span>
849
1030
  </span>
@@ -859,65 +1040,119 @@ function ComputerSessionGroup(props: {
859
1040
  );
860
1041
  }
861
1042
 
862
- function MenuButton(props: { children: ReactNode; onClick: () => void; disabled?: boolean }) {
1043
+ function MenuButton(props: {
1044
+ children: ReactNode;
1045
+ onClick: (event: MouseEvent<HTMLButtonElement>) => void;
1046
+ disabled?: boolean;
1047
+ }) {
863
1048
  return (
864
1049
  <button
865
1050
  type="button"
866
1051
  onClick={props.onClick}
867
1052
  disabled={props.disabled}
868
- 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"
1053
+ 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"
869
1054
  >
870
1055
  {props.children}
871
1056
  </button>
872
1057
  );
873
1058
  }
874
1059
 
875
- function ComputerTargetRail(props: {
1060
+ function ComputerTargetPicker(props: {
876
1061
  targets: ComputerTarget[];
877
1062
  selectedTargetId: string | null;
878
1063
  loading: boolean;
879
1064
  onSelect: (id: string) => void;
880
1065
  }) {
881
- const visualTargets = props.targets.filter(isRenderableComputerView);
1066
+ const screens = props.targets.filter((target) => target.kind === "screen");
1067
+ const advancedTargets = props.targets.filter(
1068
+ (target) =>
1069
+ target.kind === "app" || (target.kind === "window" && isRenderableComputerView(target)),
1070
+ );
1071
+ const advancedRef = useViewerMenuDismiss(advancedTargets.length > 0);
1072
+ const selected = props.targets.find((target) => target.id === props.selectedTargetId);
1073
+ const choose = (id: string, restoreFocus = false) => {
1074
+ props.onSelect(id);
1075
+ if (advancedRef.current?.open) {
1076
+ advancedRef.current.removeAttribute("open");
1077
+ if (restoreFocus) advancedRef.current.querySelector("summary")?.focus();
1078
+ }
1079
+ };
882
1080
  return (
883
- <div
884
- className="flex h-10 shrink-0 items-center gap-1 overflow-x-auto border-b border-og-border bg-og-surface-0 px-2"
885
- aria-label="Desktop views"
886
- >
887
- {visualTargets.map((target) => (
1081
+ <>
1082
+ {screens.length > 1 ? (
1083
+ <select
1084
+ aria-label="Screen"
1085
+ value={selected?.kind === "screen" ? selected.id : ""}
1086
+ onChange={(event) => choose(event.currentTarget.value)}
1087
+ 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"
1088
+ >
1089
+ <option value="" disabled hidden>
1090
+ Screen
1091
+ </option>
1092
+ {screens.map((screen, index) => (
1093
+ <option key={screen.id} value={screen.id}>
1094
+ {screen.title || `Screen ${index + 1}`}
1095
+ </option>
1096
+ ))}
1097
+ </select>
1098
+ ) : screens[0] && selected?.kind !== "screen" ? (
888
1099
  <button
889
- key={target.id}
890
1100
  type="button"
891
- onClick={() => props.onSelect(target.id)}
892
- className={cn(
893
- "flex h-7 max-w-56 shrink-0 items-center gap-1.5 rounded-og-sm border px-2 text-og-control transition",
894
- target.id === props.selectedTargetId
895
- ? "border-og-border-strong bg-og-surface-2 text-og-fg"
896
- : "border-transparent text-og-muted hover:bg-og-surface-1 hover:text-og-fg",
897
- )}
898
- aria-pressed={target.id === props.selectedTargetId}
1101
+ onClick={() => choose(screens[0]!.id)}
1102
+ 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"
899
1103
  >
900
- {target.kind === "screen" ? (
901
- <MonitorIcon className="size-3.5 shrink-0" />
902
- ) : (
903
- <PanelsTopLeftIcon className="size-3.5 shrink-0" />
904
- )}
905
- <span className="truncate">{target.title || target.applicationId || target.kind}</span>
906
- {target.focused ? (
907
- <span
908
- className="size-1.5 shrink-0 rounded-full bg-og-status-running"
909
- aria-label="Focused"
910
- />
911
- ) : null}
1104
+ <MonitorIcon className="size-3.5" /> Full desktop
912
1105
  </button>
913
- ))}
914
- {props.loading ? (
915
- <LoaderCircleIcon className="ml-1 size-3.5 animate-spin text-og-muted" />
916
1106
  ) : null}
917
- {!props.loading && visualTargets.length === 0 ? (
918
- <span className="text-og-xs text-og-subtle">Waiting for apps and screens…</span>
1107
+ {advancedTargets.length > 0 ? (
1108
+ <details ref={advancedRef} className="shrink-0">
1109
+ <summary
1110
+ aria-label="Advanced desktop views"
1111
+ title="Advanced desktop views"
1112
+ 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"
1113
+ >
1114
+ <PanelsTopLeftIcon className="size-3.5" />
1115
+ <span className="hidden @sm/computer-viewer:inline">Advanced</span>
1116
+ <ChevronDownIcon className="size-3" />
1117
+ </summary>
1118
+ <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">
1119
+ <p className="shrink-0 px-2 py-1 text-og-xs text-og-fg-subtle">Apps and windows</p>
1120
+ <div className="min-h-0 max-h-80 overflow-y-auto overscroll-contain">
1121
+ {advancedTargets.map((target) => (
1122
+ <button
1123
+ key={target.id}
1124
+ type="button"
1125
+ onClick={(event) => choose(target.id, event.detail === 0)}
1126
+ aria-pressed={target.id === props.selectedTargetId}
1127
+ className={cn(
1128
+ "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",
1129
+ target.id === props.selectedTargetId && "bg-og-surface-2",
1130
+ )}
1131
+ >
1132
+ <span className="min-w-0 flex-1">
1133
+ <span className="block truncate text-og-control text-og-fg">
1134
+ {target.title || target.applicationId || target.kind}
1135
+ </span>
1136
+ <span className="block text-og-xs text-og-fg-subtle">
1137
+ {target.kind === "app" ? "App controls" : "Window view"}
1138
+ </span>
1139
+ </span>
1140
+ {target.focused ? (
1141
+ <span
1142
+ className="size-1.5 shrink-0 rounded-full bg-og-status-running"
1143
+ aria-label="Focused"
1144
+ />
1145
+ ) : null}
1146
+ </button>
1147
+ ))}
1148
+ </div>
1149
+ </div>
1150
+ </details>
919
1151
  ) : null}
920
- </div>
1152
+ {props.loading ? (
1153
+ <LoaderCircleIcon className="size-3.5 shrink-0 animate-spin text-og-fg-muted" />
1154
+ ) : null}
1155
+ </>
921
1156
  );
922
1157
  }
923
1158
 
@@ -947,9 +1182,9 @@ function ComputerLifecyclePanel(props: {
947
1182
  <div className="grid min-h-0 flex-1 place-items-center bg-og-bg p-6">
948
1183
  <div className="max-w-sm text-center">
949
1184
  {failed ? (
950
- <CircleAlertIcon className="mx-auto size-5 text-og-status-error" />
1185
+ <CircleAlertIcon className="mx-auto size-5 text-og-danger" />
951
1186
  ) : (
952
- <LoaderCircleIcon className="mx-auto size-5 animate-spin text-og-muted" />
1187
+ <LoaderCircleIcon className="mx-auto size-5 animate-spin text-og-fg-muted" />
953
1188
  )}
954
1189
  <p className="mt-3 text-og-menu font-medium text-og-fg">
955
1190
  {generationLoss
@@ -958,7 +1193,7 @@ function ComputerLifecyclePanel(props: {
958
1193
  ? "Desktop needs attention"
959
1194
  : lifecycleLabel(props.session.lifecycle)}
960
1195
  </p>
961
- <p className="mt-1 text-og-control leading-5 text-og-muted">
1196
+ <p className="mt-1 text-og-control leading-5 text-og-fg-muted">
962
1197
  {generationLoss
963
1198
  ? "This Chrome profile is live again, but the previous desktop cannot move to the new connection. Use Browser → New browser → Connected Chrome."
964
1199
  : (computerFailureMessage(props.session) ??
@@ -984,23 +1219,41 @@ function ComputerLifecyclePanel(props: {
984
1219
  );
985
1220
  }
986
1221
 
1222
+ type ComputerQueuedAction = {
1223
+ operationId: string;
1224
+ started: Promise<boolean>;
1225
+ completion: Promise<void>;
1226
+ };
1227
+
987
1228
  function ComputerViewport(props: {
988
1229
  frame: ComputerFrame | null;
989
1230
  observation: ComputerObservation | null;
990
1231
  target: ComputerTarget | null;
991
1232
  machineLocked: boolean;
1233
+ controlUnavailable: boolean;
1234
+ inputAllowed: boolean;
992
1235
  connectionState: string;
993
1236
  connectionError: Error | null;
994
1237
  mutating: boolean;
995
1238
  backgroundActions: boolean;
1239
+ backgroundInput: boolean;
1240
+ allowForegroundInputWithinOwnedSeat: boolean;
996
1241
  clipboardEnabled: boolean;
997
- onAction: (action: ComputerAction, frame: ComputerFrame | null) => Promise<void>;
1242
+ pointerInput: boolean;
1243
+ pointerClickContinuation: boolean;
1244
+ keyboardInput: boolean;
1245
+ onAction: (
1246
+ action: ComputerAction,
1247
+ frame: ComputerFrame | null,
1248
+ operationId?: string,
1249
+ ) => Promise<ComputerActionReceipt>;
998
1250
  onReadClipboard: () => Promise<ComputerClipboard>;
999
1251
  onReconnect: () => void;
1000
1252
  onError: (cause: unknown) => void;
1001
1253
  }) {
1002
1254
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
1003
1255
  const inputRef = useRef<HTMLTextAreaElement | null>(null);
1256
+ const composingRef = useRef(false);
1004
1257
  const pointerStartRef = useRef<PointerStart | null>(null);
1005
1258
  const clickTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
1006
1259
  const lastClickRef = useRef<{
@@ -1008,6 +1261,11 @@ function ComputerViewport(props: {
1008
1261
  x: number;
1009
1262
  y: number;
1010
1263
  frame: ComputerFrame;
1264
+ clientX: number;
1265
+ clientY: number;
1266
+ canvasRect: Pick<DOMRect, "left" | "top" | "width" | "height">;
1267
+ pending: boolean;
1268
+ delivery: ComputerQueuedAction | null;
1011
1269
  } | null>(null);
1012
1270
  const wheelRef = useRef<{
1013
1271
  x: number;
@@ -1022,17 +1280,72 @@ function ComputerViewport(props: {
1022
1280
  timer: ReturnType<typeof setTimeout>;
1023
1281
  } | null>(null);
1024
1282
  const actionRef = useRef(props.onAction);
1283
+ const targetRef = useRef(props.target);
1025
1284
  const readClipboardRef = useRef(props.onReadClipboard);
1026
1285
  const errorRef = useRef(props.onError);
1027
1286
  const actionTailRef = useRef<Promise<void>>(Promise.resolve());
1287
+ const actionQueueEpochRef = useRef(0);
1288
+ const queuedTextRef = useRef<{
1289
+ action: Extract<ComputerAction, { type: "keyboard" }>;
1290
+ epoch: number;
1291
+ delivery: ComputerQueuedAction;
1292
+ } | null>(null);
1028
1293
  const queuedFrameRef = useRef<ComputerFrame | null>(null);
1294
+ const currentFrameRef = useRef<ComputerFrame | null>(props.frame);
1295
+ const paintedFrameRef = useRef<ComputerFrame | null>(null);
1296
+ const [paintedFrame, setPaintedFrame] = useState<ComputerFrame | null>(null);
1029
1297
  const decodingFrameRef = useRef(false);
1030
1298
  const mountedRef = useRef(true);
1031
1299
  actionRef.current = props.onAction;
1300
+ targetRef.current = props.target;
1032
1301
  readClipboardRef.current = props.onReadClipboard;
1033
1302
  errorRef.current = props.onError;
1303
+ const streamFailed = props.connectionState === "error";
1034
1304
  const rawInputEnabled =
1035
- !props.backgroundActions || props.target?.kind === "screen" || props.target?.focused === true;
1305
+ props.target?.kind !== "app" &&
1306
+ !streamFailed &&
1307
+ !props.controlUnavailable &&
1308
+ !props.machineLocked &&
1309
+ (props.backgroundInput ||
1310
+ // CUA may activate within its allocated Linux desktop. This does not
1311
+ // grant foreground input to an attached physical desktop.
1312
+ props.allowForegroundInputWithinOwnedSeat ||
1313
+ !props.backgroundActions ||
1314
+ props.target?.kind === "screen" ||
1315
+ props.target?.focused === true);
1316
+
1317
+ const clearBufferedInput = useCallback(() => {
1318
+ if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
1319
+ if (wheelRef.current?.timer) clearTimeout(wheelRef.current.timer);
1320
+ if (pendingTextRef.current?.timer) clearTimeout(pendingTextRef.current.timer);
1321
+ clickTimerRef.current = null;
1322
+ wheelRef.current = null;
1323
+ pendingTextRef.current = null;
1324
+ queuedTextRef.current = null;
1325
+ pointerStartRef.current = null;
1326
+ lastClickRef.current = null;
1327
+ composingRef.current = false;
1328
+ if (inputRef.current) inputRef.current.value = "";
1329
+ }, []);
1330
+
1331
+ const pointerInputEnabled = rawInputEnabled && props.pointerInput;
1332
+ const keyboardInputEnabled = rawInputEnabled && props.keyboardInput;
1333
+
1334
+ useLayoutEffect(() => {
1335
+ const canvas = canvasRef.current;
1336
+ const viewport = canvas?.parentElement;
1337
+ if (!canvas || !viewport) return;
1338
+ const fit = () => fitComputerCanvas(canvas);
1339
+ fit();
1340
+ if (typeof ResizeObserver === "undefined") {
1341
+ window.addEventListener("resize", fit);
1342
+ return () => window.removeEventListener("resize", fit);
1343
+ }
1344
+ // The canvas is absolute, so fitting it cannot resize the observed dock.
1345
+ const observer = new ResizeObserver(fit);
1346
+ observer.observe(viewport);
1347
+ return () => observer.disconnect();
1348
+ }, []);
1036
1349
 
1037
1350
  const paintQueuedFrames = useCallback(() => {
1038
1351
  if (decodingFrameRef.current) return;
@@ -1050,8 +1363,15 @@ function ComputerViewport(props: {
1050
1363
  const bitmap = await createImageBitmap(blob);
1051
1364
  try {
1052
1365
  const canvas = canvasRef.current;
1053
- if (mountedRef.current && canvas) {
1366
+ if (
1367
+ mountedRef.current &&
1368
+ canvas &&
1369
+ currentFrameRef.current &&
1370
+ sameComputerTarget(frame, currentFrameRef.current)
1371
+ ) {
1054
1372
  paintCanvas(canvas, bitmap, frame.width, frame.height);
1373
+ paintedFrameRef.current = frame;
1374
+ setPaintedFrame(frame);
1055
1375
  }
1056
1376
  } finally {
1057
1377
  bitmap.close();
@@ -1061,11 +1381,23 @@ function ComputerViewport(props: {
1061
1381
  objectUrl = URL.createObjectURL(blob);
1062
1382
  const image = await loadImage(objectUrl);
1063
1383
  const canvas = canvasRef.current;
1064
- if (mountedRef.current && canvas) {
1384
+ if (
1385
+ mountedRef.current &&
1386
+ canvas &&
1387
+ currentFrameRef.current &&
1388
+ sameComputerTarget(frame, currentFrameRef.current)
1389
+ ) {
1065
1390
  paintCanvas(canvas, image, frame.width, frame.height);
1391
+ paintedFrameRef.current = frame;
1392
+ setPaintedFrame(frame);
1066
1393
  }
1067
1394
  } catch (cause) {
1068
- if (mountedRef.current) errorRef.current(cause);
1395
+ if (
1396
+ mountedRef.current &&
1397
+ currentFrameRef.current &&
1398
+ sameComputerTarget(frame, currentFrameRef.current)
1399
+ )
1400
+ errorRef.current(cause);
1069
1401
  } finally {
1070
1402
  if (objectUrl) URL.revokeObjectURL(objectUrl);
1071
1403
  }
@@ -1077,40 +1409,127 @@ function ComputerViewport(props: {
1077
1409
  })();
1078
1410
  }, []);
1079
1411
 
1080
- useEffect(() => {
1412
+ useLayoutEffect(() => {
1081
1413
  mountedRef.current = true;
1082
1414
  return () => {
1083
1415
  mountedRef.current = false;
1416
+ actionQueueEpochRef.current += 1;
1084
1417
  queuedFrameRef.current = null;
1418
+ clearBufferedInput();
1085
1419
  };
1086
- }, []);
1420
+ }, [clearBufferedInput]);
1087
1421
 
1088
- useEffect(() => {
1089
- if (!props.frame) return;
1422
+ useLayoutEffect(() => {
1423
+ if (currentFrameRef.current && !props.frame) {
1424
+ actionQueueEpochRef.current += 1;
1425
+ clearBufferedInput();
1426
+ }
1427
+ currentFrameRef.current = props.frame;
1428
+ if (!props.frame) {
1429
+ queuedFrameRef.current = null;
1430
+ paintedFrameRef.current = null;
1431
+ setPaintedFrame(null);
1432
+ return;
1433
+ }
1090
1434
  queuedFrameRef.current = props.frame;
1091
1435
  paintQueuedFrames();
1092
- }, [paintQueuedFrames, props.frame]);
1436
+ }, [clearBufferedInput, paintQueuedFrames, props.frame]);
1093
1437
 
1094
- useEffect(
1095
- () => () => {
1096
- if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
1097
- if (wheelRef.current?.timer) clearTimeout(wheelRef.current.timer);
1098
- if (pendingTextRef.current?.timer) clearTimeout(pendingTextRef.current.timer);
1099
- },
1100
- [],
1101
- );
1438
+ useLayoutEffect(() => {
1439
+ if (!rawInputEnabled || !props.pointerInput || !props.keyboardInput) {
1440
+ actionQueueEpochRef.current += 1;
1441
+ clearBufferedInput();
1442
+ }
1443
+ }, [clearBufferedInput, props.keyboardInput, props.pointerInput, rawInputEnabled]);
1102
1444
 
1103
1445
  const enqueue = useCallback(
1104
- (action: ComputerAction, frame: ComputerFrame | null, after?: () => Promise<void>) => {
1105
- actionTailRef.current = actionTailRef.current
1106
- .catch(() => undefined)
1107
- .then(async () => {
1108
- await actionRef.current(action, frame);
1109
- await after?.();
1446
+ (
1447
+ action: ComputerAction,
1448
+ frame: ComputerFrame | null,
1449
+ after?: (isCurrent: () => boolean) => Promise<void>,
1450
+ continuation?: ComputerQueuedAction,
1451
+ ) => {
1452
+ const epoch = actionQueueEpochRef.current;
1453
+ const plainText = action.type === "keyboard" && action.action === "type";
1454
+ const queuedText = queuedTextRef.current;
1455
+ if (
1456
+ plainText &&
1457
+ !frame &&
1458
+ !after &&
1459
+ !continuation &&
1460
+ queuedText?.epoch === epoch &&
1461
+ queuedText.delivery.completion === actionTailRef.current &&
1462
+ queuedText.action.value.length + action.value.length <= 1_000_000
1463
+ ) {
1464
+ // Only combine adjacent text that has not crossed the dispatch boundary.
1465
+ queuedText.action.value += action.value;
1466
+ return queuedText.delivery;
1467
+ }
1468
+ queuedTextRef.current = null;
1469
+ const dispatch = actionRef.current;
1470
+ const admittedTarget = targetRef.current;
1471
+ const isCurrent = () => mountedRef.current && epoch === actionQueueEpochRef.current;
1472
+ const operationId = crypto.randomUUID();
1473
+ let markStarted!: (value: boolean) => void;
1474
+ const started = new Promise<boolean>((resolve) => {
1475
+ markStarted = resolve;
1476
+ });
1477
+ const previous = actionTailRef.current;
1478
+ // Only the real second click may start before its first HTTP receipt.
1479
+ // The controller queue and native causal proof still serialize delivery.
1480
+ const ready =
1481
+ continuation && previous === continuation.completion
1482
+ ? continuation.started
1483
+ : previous.then(() => true);
1484
+ const delivery = ready
1485
+ .then(async (firstStarted) => {
1486
+ if (queuedTextRef.current?.action === action) queuedTextRef.current = null;
1487
+ if (!firstStarted || !isCurrent()) {
1488
+ markStarted(false);
1489
+ return;
1490
+ }
1491
+ const pending = dispatch(action, frame, operationId);
1492
+ markStarted(true);
1493
+ const receipt = await pending;
1494
+ const isCurrentResult = () => isCurrent() && !isStaleComputerActionResult(receipt);
1495
+ if (!isCurrentResult()) return;
1496
+ const observedTarget = receipt.observation?.target;
1497
+ if (
1498
+ !admittedTarget ||
1499
+ receipt.computerSessionId !== admittedTarget.computerSessionId ||
1500
+ receipt.controllerGeneration !== admittedTarget.controllerGeneration ||
1501
+ receipt.targetId !== admittedTarget.id ||
1502
+ (observedTarget &&
1503
+ (observedTarget.computerSessionId !== admittedTarget.computerSessionId ||
1504
+ observedTarget.controllerGeneration !== admittedTarget.controllerGeneration ||
1505
+ observedTarget.id !== admittedTarget.id ||
1506
+ observedTarget.targetGeneration !== admittedTarget.targetGeneration))
1507
+ ) {
1508
+ // Receipt projection can replace the target before React remounts us.
1509
+ actionQueueEpochRef.current += 1;
1510
+ clearBufferedInput();
1511
+ return;
1512
+ }
1513
+ await after?.(isCurrentResult);
1110
1514
  })
1111
- .catch((cause) => errorRef.current(cause));
1515
+ .catch((cause) => {
1516
+ markStarted(false);
1517
+ if (!isCurrent() || isStaleComputerActionResult(cause)) return;
1518
+ actionQueueEpochRef.current += 1;
1519
+ clearBufferedInput();
1520
+ errorRef.current(cause);
1521
+ });
1522
+ const completion = continuation
1523
+ ? Promise.all([previous, delivery]).then(() => undefined)
1524
+ : delivery;
1525
+ actionTailRef.current = completion;
1526
+ const queued = { operationId, started, completion };
1527
+ if (plainText && !frame && !after && !continuation) {
1528
+ queuedTextRef.current = { action, epoch, delivery: queued };
1529
+ }
1530
+ return queued;
1112
1531
  },
1113
- [],
1532
+ [clearBufferedInput],
1114
1533
  );
1115
1534
 
1116
1535
  const point = useCallback(
@@ -1133,6 +1552,7 @@ function ComputerViewport(props: {
1133
1552
  if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
1134
1553
  clickTimerRef.current = null;
1135
1554
  lastClickRef.current = null;
1555
+ if (!pending.pending) return;
1136
1556
  enqueue(
1137
1557
  {
1138
1558
  type: "pointer",
@@ -1145,24 +1565,55 @@ function ComputerViewport(props: {
1145
1565
  );
1146
1566
  }, [enqueue]);
1147
1567
 
1568
+ const flushPendingWheel = useCallback(() => {
1569
+ const batch = wheelRef.current;
1570
+ if (!batch) return;
1571
+ if (batch.timer) clearTimeout(batch.timer);
1572
+ wheelRef.current = null;
1573
+ enqueue(
1574
+ {
1575
+ type: "pointer",
1576
+ frameId: batch.frame.frameId,
1577
+ action: "scroll",
1578
+ x: batch.x,
1579
+ y: batch.y,
1580
+ deltaX: batch.deltaX,
1581
+ deltaY: batch.deltaY,
1582
+ },
1583
+ batch.frame,
1584
+ );
1585
+ }, [enqueue]);
1586
+
1148
1587
  const pointerDown = (event: PointerEvent<HTMLCanvasElement>) => {
1149
- if (!props.frame || props.mutating || !rawInputEnabled || event.button !== 0) return;
1588
+ const frame = paintedFrameRef.current;
1589
+ if (
1590
+ !frame ||
1591
+ (!props.pointerClickContinuation && props.mutating) ||
1592
+ !rawInputEnabled ||
1593
+ event.button !== 0
1594
+ )
1595
+ return;
1596
+ // Preserve the keyboard sink's focus through the canvas pointer default action.
1597
+ event.preventDefault();
1598
+ if (keyboardInputEnabled) inputRef.current?.focus({ preventScroll: true });
1599
+ if (!pointerInputEnabled) return;
1600
+ flushPendingWheel();
1150
1601
  flushPendingText();
1151
1602
  pointerStartRef.current = {
1152
1603
  x: event.clientX,
1153
1604
  y: event.clientY,
1154
1605
  pointerId: event.pointerId,
1155
- frame: props.frame,
1606
+ frame,
1156
1607
  };
1157
1608
  event.currentTarget.setPointerCapture?.(event.pointerId);
1158
- inputRef.current?.focus({ preventScroll: true });
1159
1609
  };
1160
1610
 
1161
1611
  const pointerUp = (event: PointerEvent<HTMLCanvasElement>) => {
1162
- if (!rawInputEnabled) return;
1612
+ if (!pointerInputEnabled) return;
1163
1613
  const start = pointerStartRef.current;
1164
1614
  pointerStartRef.current = null;
1165
1615
  if (!start || start.pointerId !== event.pointerId) return;
1616
+ const canvasRect = event.currentTarget.getBoundingClientRect();
1166
1617
  const from = point(start.frame, start.x, start.y);
1167
1618
  const to = point(start.frame, event.clientX, event.clientY);
1168
1619
  if (!from || !to) return;
@@ -1187,8 +1638,18 @@ function ComputerViewport(props: {
1187
1638
  if (
1188
1639
  previous &&
1189
1640
  now - previous.at < 280 &&
1190
- Math.hypot(previous.x - to.x, previous.y - to.y) < 6 &&
1191
- sameFrameFence(previous.frame, start.frame)
1641
+ previous.pending === !props.pointerClickContinuation &&
1642
+ (props.pointerClickContinuation
1643
+ ? Math.hypot(previous.clientX - event.clientX, previous.clientY - event.clientY) < 6 &&
1644
+ previous.canvasRect.left === canvasRect.left &&
1645
+ previous.canvasRect.top === canvasRect.top &&
1646
+ previous.canvasRect.width === canvasRect.width &&
1647
+ previous.canvasRect.height === canvasRect.height &&
1648
+ sameComputerTarget(previous.frame, start.frame) &&
1649
+ previous.frame.width === start.frame.width &&
1650
+ previous.frame.height === start.frame.height
1651
+ : Math.hypot(previous.x - to.x, previous.y - to.y) < 6 &&
1652
+ sameFrameFence(previous.frame, start.frame))
1192
1653
  ) {
1193
1654
  if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
1194
1655
  clickTimerRef.current = null;
@@ -1197,15 +1658,47 @@ function ComputerViewport(props: {
1197
1658
  {
1198
1659
  type: "pointer",
1199
1660
  frameId: start.frame.frameId,
1200
- action: "double_click",
1661
+ action: props.pointerClickContinuation ? "click" : "double_click",
1662
+ ...(props.pointerClickContinuation ? { clickCount: 2 as const } : {}),
1663
+ ...(previous.delivery
1664
+ ? { continuationOfOperationId: previous.delivery.operationId }
1665
+ : {}),
1666
+ x: to.x,
1667
+ y: to.y,
1668
+ },
1669
+ start.frame,
1670
+ undefined,
1671
+ previous.delivery ?? undefined,
1672
+ );
1673
+ return;
1674
+ }
1675
+ flushPendingClick();
1676
+ lastClickRef.current = {
1677
+ at: now,
1678
+ x: to.x,
1679
+ y: to.y,
1680
+ frame: start.frame,
1681
+ clientX: event.clientX,
1682
+ clientY: event.clientY,
1683
+ canvasRect,
1684
+ pending: !props.pointerClickContinuation,
1685
+ delivery: null,
1686
+ };
1687
+ if (props.pointerClickContinuation) {
1688
+ const delivery = enqueue(
1689
+ {
1690
+ type: "pointer",
1691
+ frameId: start.frame.frameId,
1692
+ action: "click",
1693
+ clickCount: 1,
1201
1694
  x: to.x,
1202
1695
  y: to.y,
1203
1696
  },
1204
1697
  start.frame,
1205
1698
  );
1699
+ if (lastClickRef.current) lastClickRef.current.delivery = delivery;
1206
1700
  return;
1207
1701
  }
1208
- lastClickRef.current = { at: now, x: to.x, y: to.y, frame: start.frame };
1209
1702
  clickTimerRef.current = setTimeout(() => {
1210
1703
  clickTimerRef.current = null;
1211
1704
  lastClickRef.current = null;
@@ -1224,9 +1717,10 @@ function ComputerViewport(props: {
1224
1717
 
1225
1718
  const contextMenu = (event: MouseEvent<HTMLCanvasElement>) => {
1226
1719
  event.preventDefault();
1227
- if (!rawInputEnabled) return;
1228
- const frame = props.frame;
1720
+ if (!pointerInputEnabled) return;
1721
+ const frame = paintedFrameRef.current;
1229
1722
  if (!frame) return;
1723
+ flushPendingWheel();
1230
1724
  flushPendingText();
1231
1725
  flushPendingClick();
1232
1726
  const at = point(frame, event.clientX, event.clientY);
@@ -1246,45 +1740,43 @@ function ComputerViewport(props: {
1246
1740
  };
1247
1741
 
1248
1742
  const wheel = (event: WheelEvent<HTMLCanvasElement>) => {
1249
- if (!rawInputEnabled) return;
1250
- const frame = props.frame;
1743
+ if (!pointerInputEnabled) return;
1744
+ const frame = paintedFrameRef.current;
1251
1745
  if (!frame) return;
1252
1746
  const at = point(frame, event.clientX, event.clientY);
1253
1747
  if (!at) return;
1254
1748
  flushPendingText();
1255
1749
  flushPendingClick();
1256
1750
  event.preventDefault();
1257
- const pending = wheelRef.current;
1258
- if (pending?.timer) clearTimeout(pending.timer);
1751
+ let pending = wheelRef.current;
1752
+ if (
1753
+ pending &&
1754
+ (!sameComputerTarget(pending.frame, frame) ||
1755
+ Math.hypot(pending.x - at.x, pending.y - at.y) > 6)
1756
+ ) {
1757
+ flushPendingWheel();
1758
+ pending = null;
1759
+ }
1259
1760
  wheelRef.current = {
1260
1761
  x: at.x,
1261
1762
  y: at.y,
1262
- deltaX: (pending && sameFrameFence(pending.frame, frame) ? pending.deltaX : 0) + event.deltaX,
1263
- deltaY: (pending && sameFrameFence(pending.frame, frame) ? pending.deltaY : 0) + event.deltaY,
1763
+ deltaX: (pending?.deltaX ?? 0) + event.deltaX,
1764
+ deltaY: (pending?.deltaY ?? 0) + event.deltaY,
1264
1765
  frame,
1265
- timer: setTimeout(() => {
1266
- const batch = wheelRef.current;
1267
- wheelRef.current = null;
1268
- if (batch) {
1269
- enqueue(
1270
- {
1271
- type: "pointer",
1272
- frameId: batch.frame.frameId,
1273
- action: "scroll",
1274
- x: batch.x,
1275
- y: batch.y,
1276
- deltaX: batch.deltaX,
1277
- deltaY: batch.deltaY,
1278
- },
1279
- batch.frame,
1280
- );
1281
- }
1282
- }, 45),
1766
+ timer: pending?.timer ?? setTimeout(flushPendingWheel, 45),
1283
1767
  };
1284
1768
  };
1285
1769
 
1286
1770
  const keyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
1287
- if (!rawInputEnabled) return;
1771
+ if (!keyboardInputEnabled) return;
1772
+ if (
1773
+ composingRef.current ||
1774
+ event.nativeEvent.isComposing ||
1775
+ event.nativeEvent.keyCode === 229
1776
+ ) {
1777
+ return;
1778
+ }
1779
+ flushPendingWheel();
1288
1780
  const command = event.metaKey || event.ctrlKey;
1289
1781
  if (
1290
1782
  props.clipboardEnabled &&
@@ -1304,12 +1796,15 @@ function ComputerViewport(props: {
1304
1796
  };
1305
1797
 
1306
1798
  const copy = (event: ClipboardEvent<HTMLTextAreaElement>) => {
1307
- if (!props.clipboardEnabled || !rawInputEnabled) return;
1799
+ if (!props.clipboardEnabled || !keyboardInputEnabled) return;
1308
1800
  event.preventDefault();
1801
+ flushPendingWheel();
1309
1802
  flushPendingText();
1310
1803
  flushPendingClick();
1311
- enqueue({ type: "clipboard", operation: "copy" }, null, async () => {
1312
- const clipboard = await readClipboardRef.current();
1804
+ const readClipboard = readClipboardRef.current;
1805
+ enqueue({ type: "clipboard", operation: "copy" }, null, async (isCurrent) => {
1806
+ const clipboard = await readClipboard();
1807
+ if (!isCurrent()) return;
1313
1808
  if (!clipboard.text) return;
1314
1809
  if (!(await copyTextToClipboard(clipboard.text))) {
1315
1810
  throw new Error("Desktop text could not be copied to the local clipboard");
@@ -1318,19 +1813,26 @@ function ComputerViewport(props: {
1318
1813
  };
1319
1814
 
1320
1815
  const paste = (event: ClipboardEvent<HTMLTextAreaElement>) => {
1321
- if (!props.clipboardEnabled || !rawInputEnabled) return;
1816
+ if (!props.clipboardEnabled || !keyboardInputEnabled) return;
1322
1817
  event.preventDefault();
1818
+ flushPendingWheel();
1323
1819
  flushPendingText();
1324
1820
  flushPendingClick();
1325
1821
  const text = event.clipboardData.getData("text/plain");
1326
- enqueue({ type: "clipboard", operation: "write", text }, null, async () => {
1327
- assertExactComputerClipboard(await readClipboardRef.current(), text);
1328
- await actionRef.current({ type: "clipboard", operation: "paste" }, null);
1822
+ const readClipboard = readClipboardRef.current;
1823
+ const dispatch = actionRef.current;
1824
+ enqueue({ type: "clipboard", operation: "write", text }, null, async (isCurrent) => {
1825
+ const clipboard = await readClipboard();
1826
+ if (!isCurrent()) return;
1827
+ assertExactComputerClipboard(clipboard, text);
1828
+ await dispatch({ type: "clipboard", operation: "paste" }, null);
1329
1829
  });
1330
1830
  };
1331
1831
 
1332
- const input = (value: string) => {
1333
- if (!value || !rawInputEnabled) return;
1832
+ const input = (value: string, nativeComposing = false) => {
1833
+ if (composingRef.current || nativeComposing) return;
1834
+ if (!value || !keyboardInputEnabled) return;
1835
+ flushPendingWheel();
1334
1836
  flushPendingClick();
1335
1837
  const pending = pendingTextRef.current;
1336
1838
  if (pending) {
@@ -1346,7 +1848,17 @@ function ComputerViewport(props: {
1346
1848
  if (inputRef.current) inputRef.current.value = "";
1347
1849
  };
1348
1850
 
1349
- const showCanvas = props.frame !== null && !props.machineLocked;
1851
+ const compositionEnd = (event: CompositionEvent<HTMLTextAreaElement>) => {
1852
+ composingRef.current = false;
1853
+ input(event.currentTarget.value || event.data);
1854
+ };
1855
+
1856
+ const showCanvas =
1857
+ props.frame !== null &&
1858
+ paintedFrame !== null &&
1859
+ sameComputerTarget(props.frame, paintedFrame) &&
1860
+ !props.machineLocked &&
1861
+ !streamFailed;
1350
1862
  return (
1351
1863
  <div className="relative min-h-0 flex-1 overflow-hidden bg-black">
1352
1864
  <canvas
@@ -1354,7 +1866,7 @@ function ComputerViewport(props: {
1354
1866
  className={cn(
1355
1867
  "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",
1356
1868
  !showCanvas && "invisible",
1357
- !rawInputEnabled && "cursor-default",
1869
+ !pointerInputEnabled && "cursor-default",
1358
1870
  )}
1359
1871
  onPointerDown={pointerDown}
1360
1872
  onPointerUp={pointerUp}
@@ -1368,7 +1880,13 @@ function ComputerViewport(props: {
1368
1880
  <textarea
1369
1881
  ref={inputRef}
1370
1882
  defaultValue=""
1371
- onInput={(event) => input(event.currentTarget.value)}
1883
+ onInput={(event) =>
1884
+ input(event.currentTarget.value, (event.nativeEvent as InputEvent).isComposing)
1885
+ }
1886
+ onCompositionStart={() => {
1887
+ composingRef.current = true;
1888
+ }}
1889
+ onCompositionEnd={compositionEnd}
1372
1890
  onKeyDown={keyDown}
1373
1891
  onCopy={copy}
1374
1892
  onPaste={paste}
@@ -1377,12 +1895,14 @@ function ComputerViewport(props: {
1377
1895
  autoCapitalize="off"
1378
1896
  autoCorrect="off"
1379
1897
  spellCheck={false}
1380
- disabled={!rawInputEnabled}
1898
+ disabled={!keyboardInputEnabled}
1381
1899
  />
1382
1900
  {!showCanvas ? (
1383
1901
  <ComputerViewportFallback
1384
1902
  observation={props.observation}
1385
1903
  machineLocked={props.machineLocked}
1904
+ controlUnavailable={props.controlUnavailable}
1905
+ inputAllowed={props.inputAllowed}
1386
1906
  connectionState={props.connectionState}
1387
1907
  error={props.connectionError}
1388
1908
  onAction={(action) => enqueue(action, null)}
@@ -1394,17 +1914,25 @@ function ComputerViewport(props: {
1394
1914
  <LoaderCircleIcon className="size-3 animate-spin" /> Acting
1395
1915
  </div>
1396
1916
  ) : null}
1397
- {showCanvas && !rawInputEnabled ? (
1398
- <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">
1399
- <span>Background view · use native controls</span>
1400
- {props.target ? (
1917
+ {showCanvas && (!rawInputEnabled || !props.pointerInput || !props.keyboardInput) ? (
1918
+ <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">
1919
+ <span>
1920
+ {!rawInputEnabled && (props.pointerInput || props.keyboardInput)
1921
+ ? "Background view · use app controls"
1922
+ : !props.pointerInput && !props.keyboardInput
1923
+ ? "View only · mouse and keyboard unavailable"
1924
+ : props.keyboardInput
1925
+ ? "Keyboard only · mouse unavailable"
1926
+ : "Mouse only · keyboard unavailable"}
1927
+ </span>
1928
+ {!rawInputEnabled && props.target && (props.pointerInput || props.keyboardInput) ? (
1401
1929
  <button
1402
1930
  type="button"
1403
1931
  disabled={props.mutating}
1404
1932
  onClick={() => enqueue({ type: "focus", targetId: props.target!.id }, null)}
1405
- className="rounded-full bg-white/10 px-2 py-0.5 font-medium text-white transition hover:bg-white/20 disabled:opacity-50"
1933
+ 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"
1406
1934
  >
1407
- Control directly
1935
+ Bring to front
1408
1936
  </button>
1409
1937
  ) : null}
1410
1938
  </div>
@@ -1422,6 +1950,8 @@ function assertExactComputerClipboard(clipboard: ComputerClipboard, expected: st
1422
1950
  function ComputerViewportFallback(props: {
1423
1951
  observation: ComputerObservation | null;
1424
1952
  machineLocked: boolean;
1953
+ controlUnavailable: boolean;
1954
+ inputAllowed: boolean;
1425
1955
  connectionState: string;
1426
1956
  error: Error | null;
1427
1957
  onAction: (action: ComputerAction) => void;
@@ -1431,46 +1961,60 @@ function ComputerViewportFallback(props: {
1431
1961
  return (
1432
1962
  <div className="absolute inset-0 grid place-items-center bg-og-bg p-6">
1433
1963
  <div className="max-w-sm text-center">
1434
- <LockKeyholeIcon className="mx-auto size-6 text-og-muted" />
1964
+ <LockKeyholeIcon className="mx-auto size-6 text-og-fg-muted" />
1435
1965
  <p className="mt-3 text-og-menu font-medium text-og-fg">Machine locked</p>
1436
- <p className="mt-1 text-og-control leading-5 text-og-muted">
1966
+ <p className="mt-1 text-og-control leading-5 text-og-fg-muted">
1437
1967
  Live frames and input stay private until someone unlocks the machine.
1438
1968
  </p>
1439
1969
  </div>
1440
1970
  </div>
1441
1971
  );
1442
1972
  }
1973
+ const appControls = props.observation?.target.kind === "app";
1443
1974
  const controlFailure = interactionControlFailureFromError(props.error);
1444
- const interactive = semanticNodes(props.observation)
1975
+ const interactive = semanticControls(props.observation)
1445
1976
  .filter((node) => semanticAction(node) !== null)
1446
1977
  .slice(0, 10);
1447
1978
  return (
1448
1979
  <div className="absolute inset-0 grid place-items-center bg-og-bg p-6">
1449
- <div className="w-full max-w-md rounded-og-lg border border-og-border bg-og-surface-0 p-4 shadow-lg">
1980
+ <div className="w-full max-w-md rounded-og-lg border border-og-border bg-og-surface-1 p-4 shadow-lg">
1450
1981
  <div className="flex items-center gap-2">
1451
1982
  {props.error ? (
1452
- <CircleAlertIcon className="size-4 text-og-status-error" />
1983
+ <CircleAlertIcon className="size-4 text-og-danger" />
1984
+ ) : appControls ? (
1985
+ <PanelsTopLeftIcon className="size-4 text-og-fg-muted" />
1453
1986
  ) : (
1454
- <LoaderCircleIcon className="size-4 animate-spin text-og-muted" />
1987
+ <LoaderCircleIcon className="size-4 animate-spin text-og-fg-muted" />
1455
1988
  )}
1456
1989
  <p className="text-og-menu font-medium text-og-fg">
1457
1990
  {isAttachedChromeGenerationLossError(props.error)
1458
1991
  ? "Chrome reconnected—open a fresh browser/desktop."
1459
- : props.error
1460
- ? "Live view disconnected"
1461
- : computerConnectionLabel(props.connectionState)}
1992
+ : !props.inputAllowed
1993
+ ? "View only"
1994
+ : props.controlUnavailable
1995
+ ? "Desktop controls unavailable"
1996
+ : props.error
1997
+ ? "Live view disconnected"
1998
+ : appControls
1999
+ ? "App controls"
2000
+ : computerConnectionLabel(props.connectionState)}
1462
2001
  </p>
1463
2002
  </div>
1464
2003
  {props.error ? (
1465
- <p className="mt-2 text-og-control leading-5 text-og-muted">
2004
+ <p className="mt-2 text-og-control leading-5 text-og-fg-muted">
1466
2005
  {isAttachedChromeGenerationLossError(props.error)
1467
2006
  ? "Chrome reconnected—open a fresh browser/desktop. Use Browser → New browser → Connected Chrome."
1468
2007
  : (controlFailure?.message ?? props.error.message)}
1469
2008
  </p>
1470
2009
  ) : null}
1471
- {interactive.length > 0 ? (
2010
+ {!props.inputAllowed ? (
2011
+ <p className="mt-2 text-og-control leading-5 text-og-fg-muted">
2012
+ Refresh to check desktop controls.
2013
+ </p>
2014
+ ) : null}
2015
+ {interactive.length > 0 && !props.controlUnavailable && props.inputAllowed ? (
1472
2016
  <div className="mt-3 border-t border-og-border pt-3">
1473
- <p className="mb-2 text-og-xs text-og-subtle">Native controls remain available</p>
2017
+ <p className="mb-2 text-og-xs text-og-fg-subtle">App controls remain available</p>
1474
2018
  <div className="flex flex-wrap gap-1.5">
1475
2019
  {interactive.map((node) => (
1476
2020
  <button
@@ -1492,7 +2036,7 @@ function ComputerViewportFallback(props: {
1492
2036
  <button
1493
2037
  type="button"
1494
2038
  onClick={props.onReconnect}
1495
- className="mt-3 text-og-control font-medium text-og-accent hover:underline"
2039
+ className="mt-3 text-og-control font-medium og-link"
1496
2040
  >
1497
2041
  {controlFailure?.retryable === false ? "Try again" : "Reconnect"}
1498
2042
  </button>
@@ -1505,18 +2049,31 @@ function ComputerViewportFallback(props: {
1505
2049
  function ComputerSemanticPanel(props: {
1506
2050
  observation: ComputerObservation | null;
1507
2051
  mutating: boolean;
2052
+ controlUnavailable: boolean;
2053
+ inputAllowed: boolean;
1508
2054
  onAction: (action: ComputerAction) => void;
1509
2055
  }) {
1510
- const nodes = semanticNodes(props.observation).slice(0, 100);
2056
+ const nodes = semanticControls(props.observation).slice(0, 100);
1511
2057
  return (
1512
- <aside className="w-64 shrink-0 overflow-y-auto border-l border-og-border bg-og-surface-0 p-2">
1513
- <div className="mb-2 flex items-center gap-1.5 px-1 text-og-xs font-medium uppercase tracking-[0.1em] text-og-subtle">
1514
- <KeyboardIcon className="size-3" /> Native controls
2058
+ <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">
2059
+ <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">
2060
+ <KeyboardIcon className="size-3" /> App controls
1515
2061
  </div>
1516
- {nodes.length === 0 ? (
1517
- <p className="px-1 py-2 text-og-control leading-5 text-og-muted">
1518
- This view has no accessibility controls. Use the live image instead.
2062
+ {!props.inputAllowed ? (
2063
+ <p className="px-1 py-2 text-og-control leading-5 text-og-fg-muted">
2064
+ View only · refresh to check desktop controls.
1519
2065
  </p>
2066
+ ) : props.controlUnavailable ? (
2067
+ <p className="px-1 py-2 text-og-control leading-5 text-og-fg-muted">
2068
+ Reconnect to use app controls.
2069
+ </p>
2070
+ ) : null}
2071
+ {nodes.length === 0 ? (
2072
+ props.controlUnavailable ? null : (
2073
+ <p className="px-1 py-2 text-og-control leading-5 text-og-fg-muted">
2074
+ This view has no accessibility controls. Use the live image instead.
2075
+ </p>
2076
+ )
1520
2077
  ) : (
1521
2078
  <div className="space-y-0.5">
1522
2079
  {nodes.map((node) => (
@@ -1554,6 +2111,7 @@ function ComputerSemanticControl(props: {
1554
2111
  className="rounded-og-sm px-2 py-1.5 hover:bg-og-surface-2"
1555
2112
  onSubmit={(event) => {
1556
2113
  event.preventDefault();
2114
+ if (props.mutating) return;
1557
2115
  props.onAction({
1558
2116
  type: "semantic",
1559
2117
  locator: { kind: "ref", ref: node.ref },
@@ -1562,7 +2120,7 @@ function ComputerSemanticControl(props: {
1562
2120
  });
1563
2121
  }}
1564
2122
  >
1565
- <label className="block truncate text-[10px] uppercase text-og-subtle">
2123
+ <label className="block truncate text-[10px] uppercase text-og-fg-subtle">
1566
2124
  {node.name || node.identifier || node.role}
1567
2125
  </label>
1568
2126
  <div className="mt-1 flex gap-1">
@@ -1594,7 +2152,7 @@ function ComputerSemanticControl(props: {
1594
2152
  }}
1595
2153
  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"
1596
2154
  >
1597
- <span className="mt-0.5 text-[10px] uppercase text-og-subtle">{node.role}</span>
2155
+ <span className="mt-0.5 text-[10px] uppercase text-og-fg-subtle">{node.role}</span>
1598
2156
  <span className="min-w-0 flex-1 truncate text-og-control text-og-fg">
1599
2157
  {node.name || semanticValue(node) || node.identifier || "Unnamed control"}
1600
2158
  </span>
@@ -1606,6 +2164,8 @@ function ComputerStatusBar(props: {
1606
2164
  session: ComputerSession | null;
1607
2165
  target: ComputerTarget | null;
1608
2166
  connectionState: string;
2167
+ controlUnavailable: boolean;
2168
+ inputAllowed: boolean;
1609
2169
  refreshing: boolean;
1610
2170
  showControls: boolean;
1611
2171
  controlCount: number;
@@ -1613,7 +2173,7 @@ function ComputerStatusBar(props: {
1613
2173
  }) {
1614
2174
  const screen = props.target?.kind === "screen";
1615
2175
  return (
1616
- <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">
2176
+ <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">
1617
2177
  <span
1618
2178
  className={cn(
1619
2179
  "size-1.5 rounded-full",
@@ -1621,28 +2181,48 @@ function ComputerStatusBar(props: {
1621
2181
  )}
1622
2182
  />
1623
2183
  <span>
1624
- {props.connectionState === "live" ? "Live" : computerConnectionLabel(props.connectionState)}
2184
+ {!props.inputAllowed
2185
+ ? "View only"
2186
+ : props.controlUnavailable
2187
+ ? "Controls unavailable"
2188
+ : props.target?.kind === "app"
2189
+ ? "App controls"
2190
+ : props.connectionState === "live"
2191
+ ? "Live"
2192
+ : computerConnectionLabel(props.connectionState)}
1625
2193
  </span>
1626
2194
  <span className="min-w-0 flex-1 truncate">
1627
- {screen
1628
- ? "Full screen · input may move pointer and focus"
1629
- : props.session?.capabilities?.backgroundActions
1630
- ? "Window · semantic controls stay in the background"
1631
- : (props.target?.kind ?? "Desktop")}
2195
+ {!props.inputAllowed
2196
+ ? "Refresh to check desktop controls"
2197
+ : props.controlUnavailable
2198
+ ? "Reconnect to use desktop input"
2199
+ : screen
2200
+ ? "Full screen · input may move pointer and focus"
2201
+ : props.target?.kind === "app"
2202
+ ? props.session?.capabilities?.backgroundActions
2203
+ ? "App · controls work in the background"
2204
+ : "App controls"
2205
+ : props.session?.capabilities?.backgroundInput
2206
+ ? "Window · clicks and typing stay in the background"
2207
+ : props.session?.capabilities?.backgroundActions
2208
+ ? "Window · app controls work in the background"
2209
+ : (props.target?.kind ?? "Desktop")}
1632
2210
  </span>
1633
2211
  {screen ? <MousePointer2Icon className="size-3" aria-hidden /> : null}
1634
2212
  {props.refreshing ? <LoaderCircleIcon className="size-3 animate-spin" /> : null}
1635
- <button
1636
- type="button"
1637
- onClick={props.onToggleControls}
1638
- aria-pressed={props.showControls}
1639
- className={cn(
1640
- "rounded px-1.5 py-0.5 transition hover:bg-og-surface-2 hover:text-og-fg",
1641
- props.showControls && "bg-og-surface-2 text-og-fg",
1642
- )}
1643
- >
1644
- Controls {props.controlCount}
1645
- </button>
2213
+ {props.controlCount > 0 || props.showControls ? (
2214
+ <button
2215
+ type="button"
2216
+ onClick={props.onToggleControls}
2217
+ aria-pressed={props.showControls}
2218
+ className={cn(
2219
+ "rounded px-1.5 py-0.5 transition hover:bg-og-surface-2 hover:text-og-fg",
2220
+ props.showControls && "bg-og-surface-2 text-og-fg",
2221
+ )}
2222
+ >
2223
+ Controls {props.controlCount}
2224
+ </button>
2225
+ ) : null}
1646
2226
  </div>
1647
2227
  );
1648
2228
  }
@@ -1651,7 +2231,7 @@ function ComputerNotice(props: { icon: ReactNode; text: string; className?: stri
1651
2231
  return (
1652
2232
  <div
1653
2233
  className={cn(
1654
- "grid h-full place-items-center bg-og-bg text-og-control text-og-muted",
2234
+ "grid h-full place-items-center bg-og-bg text-og-control text-og-fg-muted",
1655
2235
  props.className,
1656
2236
  )}
1657
2237
  >
@@ -1663,11 +2243,37 @@ function ComputerNotice(props: { icon: ReactNode; text: string; className?: stri
1663
2243
  );
1664
2244
  }
1665
2245
 
1666
- function semanticNodes(observation: ComputerObservation | null): InteractionSemanticNode[] {
2246
+ const structuralRoles = new Set([
2247
+ "frame",
2248
+ "panel",
2249
+ "section",
2250
+ "document",
2251
+ "document web",
2252
+ "static",
2253
+ "static text",
2254
+ "heading",
2255
+ "paragraph",
2256
+ "notification",
2257
+ "separator",
2258
+ "tool bar",
2259
+ "page tab list",
2260
+ ]);
2261
+
2262
+ function semanticControls(observation: ComputerObservation | null): InteractionSemanticNode[] {
1667
2263
  if (observation?.semantic?.kind !== "snapshot") return [];
1668
2264
  const result: InteractionSemanticNode[] = [];
1669
2265
  const visit = (node: InteractionSemanticNode) => {
1670
- result.push(node);
2266
+ const role = node.role.trim().toLowerCase();
2267
+ const label = node.name?.trim() || node.identifier?.trim();
2268
+ if (
2269
+ node.actions.includes("set_value") ||
2270
+ (label &&
2271
+ label.toLowerCase() !== role &&
2272
+ !structuralRoles.has(role) &&
2273
+ semanticAction(node) !== null)
2274
+ ) {
2275
+ result.push(node);
2276
+ }
1671
2277
  node.children?.forEach(visit);
1672
2278
  };
1673
2279
  observation.semantic.roots.forEach(visit);
@@ -1699,11 +2305,26 @@ function paintCanvas(
1699
2305
  width: number,
1700
2306
  height: number,
1701
2307
  ): void {
2308
+ const dimensionsChanged = canvas.width !== width || canvas.height !== height;
1702
2309
  canvas.width = width;
1703
2310
  canvas.height = height;
1704
2311
  const context = canvas.getContext("2d", { alpha: false });
1705
2312
  if (!context) throw new Error("Desktop canvas is unavailable.");
1706
2313
  context.drawImage(image, 0, 0, width, height);
2314
+ if (dimensionsChanged) fitComputerCanvas(canvas);
2315
+ }
2316
+
2317
+ function fitComputerCanvas(canvas: HTMLCanvasElement): void {
2318
+ const viewport = canvas.parentElement;
2319
+ if (!viewport || canvas.width <= 0 || canvas.height <= 0) return;
2320
+ const scale = Math.min(
2321
+ viewport.clientWidth / canvas.width,
2322
+ viewport.clientHeight / canvas.height,
2323
+ );
2324
+ // Hidden docks retain their previous fit until their content box is visible.
2325
+ if (!Number.isFinite(scale) || scale <= 0) return;
2326
+ canvas.style.width = `${canvas.width * scale}px`;
2327
+ canvas.style.height = `${canvas.height * scale}px`;
1707
2328
  }
1708
2329
 
1709
2330
  function loadImage(url: string): Promise<HTMLImageElement> {
@@ -1764,9 +2385,13 @@ export function computerKey(
1764
2385
  }
1765
2386
 
1766
2387
  function sameFrameFence(left: ComputerFrame, right: ComputerFrame): boolean {
2388
+ return left.frameId === right.frameId && sameComputerTarget(left, right);
2389
+ }
2390
+
2391
+ function sameComputerTarget(left: ComputerFrame, right: ComputerFrame): boolean {
1767
2392
  return (
1768
- left.frameId === right.frameId &&
1769
2393
  left.computerSessionId === right.computerSessionId &&
2394
+ left.controllerGeneration === right.controllerGeneration &&
1770
2395
  left.targetId === right.targetId &&
1771
2396
  left.targetGeneration === right.targetGeneration
1772
2397
  );
@@ -1786,6 +2411,8 @@ function computerFailureMessage(session: ComputerSession): string | null {
1786
2411
  return session.placement.kind === "attached_device"
1787
2412
  ? "Chrome reconnected—open a fresh browser/desktop. Use Browser → New browser → Connected Chrome."
1788
2413
  : "The desktop connection expired. Open a new desktop.";
2414
+ case "idle_released":
2415
+ return "The desktop stopped after it went unused. Open a new desktop.";
1789
2416
  case null:
1790
2417
  return null;
1791
2418
  default: