@opengeni/react 0.46.1 → 0.52.1

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 (341) hide show
  1. package/README.md +158 -14
  2. package/dist/artifacts-document.d.ts +3 -0
  3. package/dist/artifacts-document.js +17 -0
  4. package/dist/artifacts-document.js.map +1 -0
  5. package/dist/artifacts-presentation.d.ts +3 -0
  6. package/dist/artifacts-presentation.js +17 -0
  7. package/dist/artifacts-presentation.js.map +1 -0
  8. package/dist/artifacts-spreadsheet.d.ts +3 -0
  9. package/dist/artifacts-spreadsheet.js +17 -0
  10. package/dist/artifacts-spreadsheet.js.map +1 -0
  11. package/dist/artifacts.d.ts +2 -0
  12. package/dist/artifacts.js +170 -0
  13. package/dist/artifacts.js.map +1 -0
  14. package/dist/chunk-6BBWWOOU.js +82 -0
  15. package/dist/chunk-6BBWWOOU.js.map +1 -0
  16. package/dist/{chunk-HFO4ERGQ.js → chunk-724BW4TW.js} +586 -35
  17. package/dist/chunk-724BW4TW.js.map +1 -0
  18. package/dist/chunk-7I2KB3OK.js +3895 -0
  19. package/dist/chunk-7I2KB3OK.js.map +1 -0
  20. package/dist/chunk-7WCLAEFV.js +2828 -0
  21. package/dist/chunk-7WCLAEFV.js.map +1 -0
  22. package/dist/{chunk-U3CSCMWA.js → chunk-FK6VG4LL.js} +9 -13
  23. package/dist/chunk-FK6VG4LL.js.map +1 -0
  24. package/dist/{chunk-SRFUT2ZU.js → chunk-FWS3KC3Z.js} +120 -64
  25. package/dist/chunk-FWS3KC3Z.js.map +1 -0
  26. package/dist/{chunk-EKZH6IDG.js → chunk-HDBG7XGO.js} +62 -1
  27. package/dist/chunk-HDBG7XGO.js.map +1 -0
  28. package/dist/chunk-HUIKIDM5.js +2148 -0
  29. package/dist/chunk-HUIKIDM5.js.map +1 -0
  30. package/dist/{chunk-LGVMUIRV.js → chunk-JAGDUCZQ.js} +1053 -390
  31. package/dist/chunk-JAGDUCZQ.js.map +1 -0
  32. package/dist/{chunk-LWR4MXSS.js → chunk-JR3QFY6B.js} +420 -35
  33. package/dist/chunk-JR3QFY6B.js.map +1 -0
  34. package/dist/chunk-JVYQRRC4.js +53 -0
  35. package/dist/chunk-JVYQRRC4.js.map +1 -0
  36. package/dist/chunk-KCGXPP5V.js +157 -0
  37. package/dist/chunk-KCGXPP5V.js.map +1 -0
  38. package/dist/{chunk-EB67J347.js → chunk-KZ73RL76.js} +111 -31
  39. package/dist/chunk-KZ73RL76.js.map +1 -0
  40. package/dist/chunk-PEI6RANI.js +3234 -0
  41. package/dist/chunk-PEI6RANI.js.map +1 -0
  42. package/dist/{chunk-3PB7MIT6.js → chunk-ROIRTM37.js} +220 -72
  43. package/dist/chunk-ROIRTM37.js.map +1 -0
  44. package/dist/chunk-VZTQ73XT.js +86 -0
  45. package/dist/chunk-VZTQ73XT.js.map +1 -0
  46. package/dist/{chunk-VCBFUSUZ.js → chunk-WSK7G5LE.js} +72 -4
  47. package/dist/chunk-WSK7G5LE.js.map +1 -0
  48. package/dist/{chunk-FGZUCXZF.js → chunk-WYA65Y3F.js} +16 -16
  49. package/dist/chunk-WYA65Y3F.js.map +1 -0
  50. package/dist/{chunk-6EDV6YFE.js → chunk-X4S56Y2H.js} +147 -35
  51. package/dist/chunk-X4S56Y2H.js.map +1 -0
  52. package/dist/client.d.ts +7 -1
  53. package/dist/clipboard.d.ts +1 -0
  54. package/dist/clipboard.js +7 -0
  55. package/dist/clipboard.js.map +1 -0
  56. package/dist/commands/index.d.ts +3 -3
  57. package/dist/commands/registry.d.ts +1 -1
  58. package/dist/commands/types.d.ts +1 -1
  59. package/dist/components/approval-surface.d.ts +2 -2
  60. package/dist/components/artifacts/artifact-surface.d.ts +18 -0
  61. package/dist/components/artifacts/document-editor.d.ts +172 -0
  62. package/dist/components/artifacts/editable-artifact-ui.d.ts +13 -0
  63. package/dist/components/artifacts/editable-artifact-workbench.d.ts +49 -0
  64. package/dist/components/artifacts/editable-document.d.ts +24 -0
  65. package/dist/components/artifacts/editable-presentation.d.ts +24 -0
  66. package/dist/components/artifacts/editable-spreadsheet.d.ts +32 -0
  67. package/dist/components/artifacts/index.d.ts +8 -0
  68. package/dist/components/artifacts/presentation-editor.d.ts +297 -0
  69. package/dist/components/artifacts/spreadsheet-canvas.d.ts +120 -0
  70. package/dist/components/artifacts/spreadsheet-grid.d.ts +113 -0
  71. package/dist/components/browser-viewer.d.ts +29 -0
  72. package/dist/components/chat-composer.d.ts +13 -8
  73. package/dist/components/code-editor.d.ts +25 -3
  74. package/dist/components/command-palette.d.ts +2 -2
  75. package/dist/components/composer-transcription-control.d.ts +4 -2
  76. package/dist/components/composer.d.ts +29 -15
  77. package/dist/components/computer-viewer.d.ts +23 -0
  78. package/dist/components/copy-button.d.ts +2 -2
  79. package/dist/components/desktop-viewer.d.ts +25 -21
  80. package/dist/components/diff-view.d.ts +1 -1
  81. package/dist/components/enrollment-consent.d.ts +1 -1
  82. package/dist/components/enrollment-device-flow.d.ts +1 -1
  83. package/dist/components/file-browser.d.ts +2 -2
  84. package/dist/components/fleet-tile.d.ts +1 -1
  85. package/dist/components/generated-video-player.d.ts +10 -0
  86. package/dist/components/human-input-form.d.ts +1 -1
  87. package/dist/components/human-input-surface.d.ts +2 -2
  88. package/dist/components/interactive-workbench-viewers.d.ts +2 -0
  89. package/dist/components/machine-card.d.ts +2 -2
  90. package/dist/components/machine-dock-bar.d.ts +3 -3
  91. package/dist/components/machine-health-pill.d.ts +2 -2
  92. package/dist/components/machine-input-display.d.ts +1 -1
  93. package/dist/components/machine-metrics.d.ts +2 -2
  94. package/dist/components/machine-status-pill.d.ts +4 -4
  95. package/dist/components/machines/health.d.ts +1 -1
  96. package/dist/components/machines/machine-detail.d.ts +5 -3
  97. package/dist/components/machines/metric-history-chart.d.ts +1 -1
  98. package/dist/components/machines/metric-sparkline.d.ts +2 -2
  99. package/dist/components/machines/series.d.ts +2 -2
  100. package/dist/components/machines-dashboard.d.ts +5 -5
  101. package/dist/components/markdown.d.ts +1 -1
  102. package/dist/components/message-timeline.d.ts +22 -5
  103. package/dist/components/model-picker.d.ts +2 -2
  104. package/dist/components/model-policy-picker.d.ts +7 -7
  105. package/dist/components/pierre-diff.d.ts +1 -1
  106. package/dist/components/pierre-file.d.ts +1 -1
  107. package/dist/components/queue-draft-policy.d.ts +1 -1
  108. package/dist/components/queue-surface-implementation.d.ts +3 -3
  109. package/dist/components/queue-surface-state.d.ts +4 -4
  110. package/dist/components/queue-surface.d.ts +5 -5
  111. package/dist/components/sandbox-files.d.ts +5 -5
  112. package/dist/components/sandbox-terminal.d.ts +50 -4
  113. package/dist/components/sandbox-workspace.d.ts +30 -10
  114. package/dist/components/session-chrome.d.ts +4 -4
  115. package/dist/components/session-status.d.ts +2 -2
  116. package/dist/components/timeline-annotation-selection.d.ts +9 -0
  117. package/dist/components/timeline-annotations-dialog.d.ts +16 -0
  118. package/dist/components/timeline-annotations.d.ts +13 -0
  119. package/dist/components/tooltip.d.ts +4 -4
  120. package/dist/components/user-message-body.d.ts +2 -2
  121. package/dist/components/workbench-changes.d.ts +2 -2
  122. package/dist/components/workspace-dock.d.ts +3 -1
  123. package/dist/composer.d.ts +11 -11
  124. package/dist/composer.js +7 -7
  125. package/dist/hooks/internal.d.ts +7 -1
  126. package/dist/hooks/use-attached-browsers.d.ts +21 -0
  127. package/dist/hooks/use-available-models.d.ts +2 -2
  128. package/dist/hooks/use-billing-usage.d.ts +1 -1
  129. package/dist/hooks/use-browser-frame-stream.d.ts +28 -0
  130. package/dist/hooks/use-browser-identities.d.ts +26 -0
  131. package/dist/hooks/use-browser-session.d.ts +28 -0
  132. package/dist/hooks/use-browser-sessions.d.ts +32 -0
  133. package/dist/hooks/use-codex-accounts.d.ts +3 -3
  134. package/dist/hooks/use-composer.d.ts +36 -9
  135. package/dist/hooks/use-computer-frame-stream.d.ts +24 -0
  136. package/dist/hooks/use-computer-session.d.ts +28 -0
  137. package/dist/hooks/use-computer-sessions.d.ts +28 -0
  138. package/dist/hooks/use-desktop-stream.d.ts +1 -1
  139. package/dist/hooks/use-environments.d.ts +11 -5
  140. package/dist/hooks/use-file-attachments.d.ts +1 -1
  141. package/dist/hooks/use-goal.d.ts +2 -2
  142. package/dist/hooks/use-human-input.d.ts +2 -2
  143. package/dist/hooks/use-last-started-turn-policy.d.ts +2 -2
  144. package/dist/hooks/use-machine-chip.d.ts +2 -2
  145. package/dist/hooks/use-machines.d.ts +10 -3
  146. package/dist/hooks/use-packs.d.ts +7 -2
  147. package/dist/hooks/use-rigs.d.ts +1 -1
  148. package/dist/hooks/use-sandbox-files.d.ts +25 -4
  149. package/dist/hooks/use-sandbox-git.d.ts +13 -5
  150. package/dist/hooks/use-sandbox-terminal.d.ts +1 -1
  151. package/dist/hooks/use-scheduled-tasks.d.ts +1 -1
  152. package/dist/hooks/use-session-capabilities.d.ts +5 -4
  153. package/dist/hooks/use-session-control.d.ts +1 -1
  154. package/dist/hooks/use-session-events.d.ts +2 -2
  155. package/dist/hooks/use-session-lineage.d.ts +1 -1
  156. package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
  157. package/dist/hooks/use-session.d.ts +2 -2
  158. package/dist/hooks/use-slash-commands.d.ts +1 -1
  159. package/dist/hooks/use-terminal-stream.d.ts +8 -2
  160. package/dist/hooks/use-transcription.d.ts +2 -2
  161. package/dist/hooks/use-turn-queue.d.ts +2 -2
  162. package/dist/hooks/use-video-artifact-playback.d.ts +9 -0
  163. package/dist/hooks/use-voice-input.d.ts +6 -4
  164. package/dist/hooks/use-workspace-capture.d.ts +1 -1
  165. package/dist/hooks/use-workspace-edit.d.ts +1 -1
  166. package/dist/hooks/use-workspace-sessions.d.ts +1 -1
  167. package/dist/hooks/use-workspaces.d.ts +1 -1
  168. package/dist/index.d.ts +158 -154
  169. package/dist/index.js +1718 -908
  170. package/dist/index.js.map +1 -1
  171. package/dist/interaction.d.ts +23 -0
  172. package/dist/interaction.js +36 -0
  173. package/dist/interaction.js.map +1 -0
  174. package/dist/interactive-workbench-viewers-KHNOHWQE.js +13 -0
  175. package/dist/interactive-workbench-viewers-KHNOHWQE.js.map +1 -0
  176. package/dist/lib/composer-responsive-context.d.ts +9 -0
  177. package/dist/lib/relay-wire.d.ts +4 -0
  178. package/dist/lib/sandbox-liveness.d.ts +9 -0
  179. package/dist/lib/terminal-capability.d.ts +5 -0
  180. package/dist/lib/use-portal-token-style.d.ts +14 -3
  181. package/dist/lib/xterm-theme.d.ts +1 -1
  182. package/dist/machines.d.ts +30 -30
  183. package/dist/machines.js +3 -3
  184. package/dist/provider.d.ts +4 -4
  185. package/dist/{queue-surface-implementation-J4XJFKRW.js → queue-surface-implementation-2KVJTYXH.js} +117 -64
  186. package/dist/queue-surface-implementation-2KVJTYXH.js.map +1 -0
  187. package/dist/realtime/dropdown-menu.d.ts +8 -8
  188. package/dist/realtime/realtime-control.d.ts +15 -6
  189. package/dist/realtime.d.ts +4 -4
  190. package/dist/realtime.js +30 -33
  191. package/dist/realtime.js.map +1 -1
  192. package/dist/session-context.d.ts +19 -1
  193. package/dist/session-ui.d.ts +14 -14
  194. package/dist/session-ui.js +6 -5
  195. package/dist/session.d.ts +28 -28
  196. package/dist/session.js +5 -5
  197. package/dist/timeline/activity-rail.d.ts +6 -3
  198. package/dist/timeline/disclosure-context.d.ts +1 -1
  199. package/dist/timeline/fleet-decision-projection.d.ts +1 -1
  200. package/dist/timeline/fleet-decision-row.d.ts +3 -3
  201. package/dist/timeline/index.d.ts +19 -19
  202. package/dist/timeline/parsers.d.ts +6 -3
  203. package/dist/timeline/projection.d.ts +3 -3
  204. package/dist/timeline/registry.d.ts +10 -1
  205. package/dist/timeline/screenshot-lightbox.d.ts +2 -2
  206. package/dist/timeline/shared.d.ts +21 -10
  207. package/dist/timeline/tool-diff.d.ts +2 -2
  208. package/dist/timeline/tool-renderers.d.ts +1 -1
  209. package/dist/timeline/turn-summary.d.ts +3 -3
  210. package/dist/timeline/types.d.ts +42 -2
  211. package/dist/timeline-annotation-selection-3L7LHGG2.js +187 -0
  212. package/dist/timeline-annotation-selection-3L7LHGG2.js.map +1 -0
  213. package/dist/timeline-annotations-dialog-U7EVLR75.js +196 -0
  214. package/dist/timeline-annotations-dialog-U7EVLR75.js.map +1 -0
  215. package/dist/voice-recording-store.d.ts +8 -2
  216. package/package.json +52 -7
  217. package/src/artifacts-document.ts +31 -0
  218. package/src/artifacts-presentation.ts +36 -0
  219. package/src/artifacts-spreadsheet.ts +21 -0
  220. package/src/artifacts.ts +86 -0
  221. package/src/client.ts +90 -1
  222. package/src/clipboard.ts +2 -0
  223. package/src/components/artifacts/artifact-surface.tsx +93 -0
  224. package/src/components/artifacts/document-editor.tsx +2255 -0
  225. package/src/components/artifacts/editable-artifact-ui.tsx +89 -0
  226. package/src/components/artifacts/editable-artifact-workbench.tsx +203 -0
  227. package/src/components/artifacts/editable-document.tsx +657 -0
  228. package/src/components/artifacts/editable-presentation.tsx +1020 -0
  229. package/src/components/artifacts/editable-spreadsheet.tsx +733 -0
  230. package/src/components/artifacts/index.ts +99 -0
  231. package/src/components/artifacts/presentation-editor.tsx +3148 -0
  232. package/src/components/artifacts/spreadsheet-canvas.ts +799 -0
  233. package/src/components/artifacts/spreadsheet-grid.tsx +2129 -0
  234. package/src/components/browser-viewer.tsx +1592 -0
  235. package/src/components/chat-composer.tsx +25 -2
  236. package/src/components/code-editor.tsx +90 -31
  237. package/src/components/command-palette.tsx +1 -1
  238. package/src/components/composer-transcription-control.tsx +51 -22
  239. package/src/components/composer.tsx +49 -23
  240. package/src/components/computer-viewer.tsx +1139 -0
  241. package/src/components/copy-button.tsx +1 -1
  242. package/src/components/desktop-viewer.tsx +103 -75
  243. package/src/components/file-browser.tsx +39 -17
  244. package/src/components/generated-video-player.tsx +89 -0
  245. package/src/components/human-input-form.tsx +2 -2
  246. package/src/components/interactive-workbench-viewers.ts +2 -0
  247. package/src/components/machine-card.tsx +1 -1
  248. package/src/components/machine-input-display.ts +3 -0
  249. package/src/components/machines/machine-detail.tsx +39 -15
  250. package/src/components/machines-dashboard.tsx +7 -6
  251. package/src/components/message-timeline.tsx +327 -37
  252. package/src/components/model-picker.tsx +2 -2
  253. package/src/components/model-policy-picker.tsx +16 -15
  254. package/src/components/queue-surface-implementation.tsx +135 -75
  255. package/src/components/queue-surface-state.tsx +2 -2
  256. package/src/components/sandbox-files.tsx +142 -62
  257. package/src/components/sandbox-terminal.tsx +119 -35
  258. package/src/components/sandbox-workspace.tsx +394 -228
  259. package/src/components/session-chrome.tsx +67 -27
  260. package/src/components/timeline-annotation-selection.tsx +221 -0
  261. package/src/components/timeline-annotations-dialog.tsx +213 -0
  262. package/src/components/timeline-annotations.tsx +84 -0
  263. package/src/components/tooltip.tsx +13 -16
  264. package/src/components/user-message-body.tsx +1 -1
  265. package/src/components/workbench-changes.tsx +192 -35
  266. package/src/components/workspace-dock.tsx +13 -5
  267. package/src/composer.ts +3 -0
  268. package/src/hooks/internal.ts +132 -38
  269. package/src/hooks/use-attached-browsers.ts +152 -0
  270. package/src/hooks/use-browser-frame-stream.ts +316 -0
  271. package/src/hooks/use-browser-identities.ts +225 -0
  272. package/src/hooks/use-browser-session.ts +476 -0
  273. package/src/hooks/use-browser-sessions.ts +281 -0
  274. package/src/hooks/use-codex-accounts.ts +4 -4
  275. package/src/hooks/use-composer.ts +769 -24
  276. package/src/hooks/use-computer-frame-stream.ts +311 -0
  277. package/src/hooks/use-computer-session.ts +442 -0
  278. package/src/hooks/use-computer-sessions.ts +245 -0
  279. package/src/hooks/use-environments.ts +24 -5
  280. package/src/hooks/use-file-attachments.ts +70 -32
  281. package/src/hooks/use-goal.ts +17 -5
  282. package/src/hooks/use-machine-chip.ts +5 -3
  283. package/src/hooks/use-machines.ts +79 -3
  284. package/src/hooks/use-packs.ts +51 -0
  285. package/src/hooks/use-sandbox-files.ts +382 -88
  286. package/src/hooks/use-sandbox-git.ts +208 -46
  287. package/src/hooks/use-sandbox-terminal.ts +3 -2
  288. package/src/hooks/use-session-capabilities.ts +94 -70
  289. package/src/hooks/use-session-events.ts +18 -4
  290. package/src/hooks/use-terminal-stream.ts +102 -23
  291. package/src/hooks/use-transcription.ts +4 -14
  292. package/src/hooks/use-turn-queue.ts +17 -5
  293. package/src/hooks/use-video-artifact-playback.ts +28 -0
  294. package/src/hooks/use-voice-input.ts +163 -19
  295. package/src/hooks/use-windowed-sections.ts +9 -7
  296. package/src/hooks/use-workspace-edit.ts +2 -1
  297. package/src/index.ts +21 -6
  298. package/src/interaction.ts +71 -0
  299. package/src/lib/composer-responsive-context.ts +15 -0
  300. package/src/lib/relay-wire.ts +4 -0
  301. package/src/lib/sandbox-liveness.ts +11 -0
  302. package/src/lib/terminal-capability.ts +10 -0
  303. package/src/lib/use-portal-token-style.ts +60 -15
  304. package/src/provider.tsx +52 -7
  305. package/src/realtime/dropdown-menu.tsx +15 -19
  306. package/src/realtime/realtime-control.tsx +16 -5
  307. package/src/session-context.ts +90 -0
  308. package/src/timeline/activity-rail.tsx +31 -6
  309. package/src/timeline/index.ts +4 -1
  310. package/src/timeline/parsers.ts +46 -20
  311. package/src/timeline/projection.ts +233 -13
  312. package/src/timeline/registry.ts +19 -0
  313. package/src/timeline/screenshot-lightbox.tsx +30 -9
  314. package/src/timeline/shared.tsx +59 -11
  315. package/src/timeline/tool-renderers.tsx +421 -24
  316. package/src/timeline/turn-summary.tsx +2 -2
  317. package/src/timeline/types.ts +58 -2
  318. package/src/voice-recording-store.ts +15 -0
  319. package/styles/compiled.css +5298 -0
  320. package/styles/compiled.d.ts +2 -0
  321. package/styles/effective-tokens.css +27 -0
  322. package/styles/index.css +18 -0
  323. package/styles/responsive.css +208 -0
  324. package/styles/responsive.d.ts +2 -0
  325. package/styles/tokens.css +50 -17
  326. package/dist/chunk-3PB7MIT6.js.map +0 -1
  327. package/dist/chunk-6EDV6YFE.js.map +0 -1
  328. package/dist/chunk-EB67J347.js.map +0 -1
  329. package/dist/chunk-EKZH6IDG.js.map +0 -1
  330. package/dist/chunk-FGZUCXZF.js.map +0 -1
  331. package/dist/chunk-HFO4ERGQ.js.map +0 -1
  332. package/dist/chunk-JB5725BI.js +0 -48
  333. package/dist/chunk-JB5725BI.js.map +0 -1
  334. package/dist/chunk-LGVMUIRV.js.map +0 -1
  335. package/dist/chunk-LWR4MXSS.js.map +0 -1
  336. package/dist/chunk-SRFUT2ZU.js.map +0 -1
  337. package/dist/chunk-U3CSCMWA.js.map +0 -1
  338. package/dist/chunk-VCBFUSUZ.js.map +0 -1
  339. package/dist/fleet-decision-projection-SLNZMS42.js +0 -241
  340. package/dist/fleet-decision-projection-SLNZMS42.js.map +0 -1
  341. package/dist/queue-surface-implementation-J4XJFKRW.js.map +0 -1
@@ -0,0 +1,1139 @@
1
+ import type {
2
+ ComputerAction,
3
+ ComputerFrame,
4
+ ComputerObservation,
5
+ ComputerSession,
6
+ ComputerTarget,
7
+ InteractionSemanticNode,
8
+ } from "@opengeni/sdk/interaction";
9
+ import {
10
+ ChevronDownIcon,
11
+ CircleAlertIcon,
12
+ KeyboardIcon,
13
+ LoaderCircleIcon,
14
+ LockKeyholeIcon,
15
+ MonitorIcon,
16
+ MousePointer2Icon,
17
+ PanelsTopLeftIcon,
18
+ PlusIcon,
19
+ RefreshCwIcon,
20
+ RotateCcwIcon,
21
+ } from "lucide-react";
22
+ import {
23
+ type KeyboardEvent,
24
+ type MouseEvent,
25
+ type PointerEvent,
26
+ type ReactNode,
27
+ type WheelEvent,
28
+ useCallback,
29
+ useEffect,
30
+ useMemo,
31
+ useRef,
32
+ useState,
33
+ } from "react";
34
+ import {
35
+ type ComputerFrameWebSocketFactory,
36
+ useComputerFrameStream,
37
+ } from "../hooks/use-computer-frame-stream";
38
+ import { useComputerSession } from "../hooks/use-computer-session";
39
+ import { useComputerSessions } from "../hooks/use-computer-sessions";
40
+ import { cn } from "../lib/cn";
41
+ import type { EmbeddedComputerInteractionClientOverride } from "../session-context";
42
+
43
+ export type ComputerViewerNotification = { kind: "error" | "info"; message: string };
44
+
45
+ export type ComputerViewerProps = EmbeddedComputerInteractionClientOverride & {
46
+ /** Selected OpenGeni agent/session. Peer ComputerSessions remain visible. */
47
+ sessionId: string;
48
+ enabled?: boolean | undefined;
49
+ className?: string | undefined;
50
+ onNotify?: ((notification: ComputerViewerNotification) => void) | undefined;
51
+ /** Tests/demos only. Production uses the browser's native WebSocket. */
52
+ webSocketFactory?: ComputerFrameWebSocketFactory | undefined;
53
+ renderEmpty?: ((create: () => void, creating: boolean) => ReactNode) | undefined;
54
+ /** Host navigation request for one exact ComputerSession. */
55
+ requestedComputerSessionId?: string | null | undefined;
56
+ requestedComputerRequestId?: string | number | null | undefined;
57
+ };
58
+
59
+ type ComputerSelection = { sessionId: string; pinned: boolean } | null;
60
+ type PointerStart = { x: number; y: number; pointerId: number; frame: ComputerFrame };
61
+
62
+ /** Full ComputerSession surface: workspace discovery, peer switching, app/window/
63
+ * screen navigation, semantic controls, exact-frame input, and live media. */
64
+ export function ComputerViewer({
65
+ sessionId,
66
+ enabled = true,
67
+ className,
68
+ onNotify,
69
+ webSocketFactory,
70
+ renderEmpty,
71
+ requestedComputerSessionId,
72
+ requestedComputerRequestId,
73
+ ...override
74
+ }: ComputerViewerProps) {
75
+ const registry = useComputerSessions({ ...override, sessionId, enabled });
76
+ const createSession = registry.create;
77
+ const refreshRegistry = registry.refresh;
78
+ const liveSessions = useMemo(
79
+ () => registry.sessions.filter((session) => isLiveComputer(session)),
80
+ [registry.sessions],
81
+ );
82
+ const relevant = useMemo(
83
+ () => registry.relevantSessions.filter((session) => isLiveComputer(session)),
84
+ [registry.relevantSessions],
85
+ );
86
+ const [selection, setSelection] = useState<ComputerSelection>(null);
87
+ const [creating, setCreating] = useState(false);
88
+ const [showControls, setShowControls] = useState(false);
89
+ const previousSessionIdRef = useRef(sessionId);
90
+ const handledRequestRef = useRef<string | null>(null);
91
+
92
+ const notifyError = useCallback(
93
+ (cause: unknown, fallback: string) => {
94
+ onNotify?.({
95
+ kind: "error",
96
+ message: cause instanceof Error ? cause.message : fallback,
97
+ });
98
+ },
99
+ [onNotify],
100
+ );
101
+
102
+ useEffect(() => {
103
+ if (previousSessionIdRef.current === sessionId) return;
104
+ previousSessionIdRef.current = sessionId;
105
+ handledRequestRef.current = null;
106
+ setSelection(null);
107
+ }, [sessionId]);
108
+
109
+ useEffect(() => {
110
+ const requested = requestedComputerSessionId
111
+ ? (liveSessions.find((session) => session.id === requestedComputerSessionId) ?? null)
112
+ : null;
113
+ const requestKey = requested
114
+ ? `${requestedComputerRequestId ?? "default"}:${requested.id}`
115
+ : null;
116
+ if (requested && requestKey && handledRequestRef.current !== requestKey) {
117
+ handledRequestRef.current = requestKey;
118
+ setSelection({ sessionId: requested.id, pinned: true });
119
+ return;
120
+ }
121
+
122
+ const selectedStillLive = liveSessions.some((session) => session.id === selection?.sessionId);
123
+ const preferred = relevant[0] ?? liveSessions[0] ?? null;
124
+ if (!preferred) {
125
+ if (selection) setSelection(null);
126
+ return;
127
+ }
128
+ if (!selectedStillLive || !selection) {
129
+ setSelection({ sessionId: preferred.id, pinned: false });
130
+ return;
131
+ }
132
+ if (!selection.pinned && relevant[0] && selection.sessionId !== relevant[0].id) {
133
+ setSelection({ sessionId: relevant[0].id, pinned: false });
134
+ }
135
+ }, [liveSessions, relevant, requestedComputerRequestId, requestedComputerSessionId, selection]);
136
+
137
+ const selectedRegistrySession = useMemo(
138
+ () => liveSessions.find((session) => session.id === selection?.sessionId) ?? null,
139
+ [liveSessions, selection?.sessionId],
140
+ );
141
+ const controllerReady = selectedRegistrySession?.lifecycle === "active";
142
+ const computer = useComputerSession({
143
+ ...override,
144
+ computerSessionId: selection?.sessionId ?? null,
145
+ enabled: enabled && selection !== null && controllerReady,
146
+ });
147
+ const act = computer.act;
148
+ const actFromFrame = computer.actFromFrame;
149
+ const refreshComputer = computer.refresh;
150
+ const frames = useComputerFrameStream({
151
+ ...override,
152
+ computerSessionId: selection?.sessionId ?? null,
153
+ targetId: computer.selectedTarget?.id ?? null,
154
+ enabled: enabled && selection !== null && controllerReady && computer.selectedTarget !== null,
155
+ stream: { format: "jpeg", quality: 82, maxWidth: 1_920, maxHeight: 1_200 },
156
+ ...(webSocketFactory ? { webSocketFactory } : {}),
157
+ });
158
+ const displayedFrame =
159
+ frames.frame &&
160
+ frames.frame.computerSessionId === selection?.sessionId &&
161
+ frames.frame.targetId === computer.selectedTarget?.id
162
+ ? frames.frame
163
+ : null;
164
+ const machineLocked = selectedRegistrySession?.failureCode === "machine_locked";
165
+ const currentIds = useMemo(() => new Set(relevant.map((session) => session.id)), [relevant]);
166
+
167
+ const createComputer = useCallback(() => {
168
+ if (creating) return;
169
+ setCreating(true);
170
+ void createSession({ sessionId, name: "Computer" })
171
+ .then((response) => {
172
+ setSelection({ sessionId: response.session.id, pinned: false });
173
+ })
174
+ .catch((cause) => notifyError(cause, "Could not open a computer."))
175
+ .finally(() => setCreating(false));
176
+ }, [createSession, creating, notifyError, sessionId]);
177
+
178
+ const perform = useCallback(
179
+ async (action: ComputerAction, frame: ComputerFrame | null): Promise<void> => {
180
+ if (action.type === "pointer") {
181
+ if (!frame) throw new Error("Computer view is not ready for pointer input.");
182
+ await actFromFrame(action, frame);
183
+ } else {
184
+ await act(action);
185
+ }
186
+ },
187
+ [act, actFromFrame],
188
+ );
189
+
190
+ if (!enabled) return null;
191
+ if (registry.loading && liveSessions.length === 0) {
192
+ return (
193
+ <ComputerNotice
194
+ icon={<LoaderCircleIcon className="size-4 animate-spin" />}
195
+ text="Finding workspace computers…"
196
+ {...(className ? { className } : {})}
197
+ />
198
+ );
199
+ }
200
+ if (liveSessions.length === 0) {
201
+ if (renderEmpty) return renderEmpty(createComputer, creating);
202
+ return (
203
+ <div className={cn("grid h-full place-items-center bg-og-bg p-6", className)}>
204
+ <div className="max-w-sm text-center">
205
+ <span className="mx-auto grid size-10 place-items-center rounded-og-lg border border-og-border bg-og-surface-1 text-og-muted">
206
+ <MonitorIcon className="size-5" />
207
+ </span>
208
+ <p className="mt-3 text-og-menu font-medium text-og-fg">No computer open</p>
209
+ <p className="mt-1 text-og-control leading-5 text-og-muted">
210
+ Open this agent&apos;s desktop, or switch to any computer another workspace agent uses.
211
+ </p>
212
+ <button
213
+ type="button"
214
+ onClick={createComputer}
215
+ disabled={creating}
216
+ 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"
217
+ >
218
+ {creating ? (
219
+ <LoaderCircleIcon className="size-3.5 animate-spin" />
220
+ ) : (
221
+ <PlusIcon className="size-3.5" />
222
+ )}
223
+ Open computer
224
+ </button>
225
+ {registry.error ? (
226
+ <p className="mt-3 text-og-control text-og-status-error">{registry.error.message}</p>
227
+ ) : null}
228
+ </div>
229
+ </div>
230
+ );
231
+ }
232
+
233
+ return (
234
+ <div className={cn("flex h-full min-h-0 flex-col overflow-hidden bg-og-bg", className)}>
235
+ <ComputerToolbar
236
+ sessions={liveSessions}
237
+ relevantSessionIds={currentIds}
238
+ selectedSessionId={selection?.sessionId ?? null}
239
+ creating={creating}
240
+ refreshing={registry.refreshing}
241
+ onSelect={(computerSessionId) =>
242
+ setSelection({ sessionId: computerSessionId, pinned: true })
243
+ }
244
+ onFollow={() => {
245
+ if (relevant[0]) setSelection({ sessionId: relevant[0].id, pinned: false });
246
+ }}
247
+ onCreate={createComputer}
248
+ onRefresh={() => void Promise.all([refreshRegistry(), refreshComputer()])}
249
+ />
250
+ {selectedRegistrySession && !controllerReady ? (
251
+ <ComputerLifecyclePanel session={selectedRegistrySession} onRefresh={registry.refresh} />
252
+ ) : (
253
+ <>
254
+ <ComputerTargetRail
255
+ targets={computer.targets}
256
+ selectedTargetId={computer.selectedTarget?.id ?? null}
257
+ loading={computer.loading}
258
+ onSelect={(targetId) =>
259
+ void computer
260
+ .selectTarget(targetId)
261
+ .catch((cause) => notifyError(cause, "Could not switch computer views."))
262
+ }
263
+ />
264
+ <div className="flex min-h-0 flex-1">
265
+ <ComputerViewport
266
+ frame={machineLocked ? null : displayedFrame}
267
+ observation={computer.observation}
268
+ target={computer.selectedTarget}
269
+ machineLocked={machineLocked}
270
+ connectionState={frames.state}
271
+ connectionError={frames.error ?? computer.error}
272
+ mutating={computer.mutating}
273
+ onAction={perform}
274
+ onReconnect={frames.reconnect}
275
+ onError={(cause) => notifyError(cause, "Computer input failed.")}
276
+ />
277
+ {showControls ? (
278
+ <ComputerSemanticPanel
279
+ observation={computer.observation}
280
+ mutating={computer.mutating}
281
+ onAction={(action) =>
282
+ void perform(action, null).catch((cause) =>
283
+ notifyError(cause, "Computer action failed."),
284
+ )
285
+ }
286
+ />
287
+ ) : null}
288
+ </div>
289
+ <ComputerStatusBar
290
+ session={computer.session}
291
+ target={computer.selectedTarget}
292
+ connectionState={frames.state}
293
+ refreshing={registry.refreshing}
294
+ showControls={showControls}
295
+ controlCount={semanticNodes(computer.observation).length}
296
+ onToggleControls={() => setShowControls((current) => !current)}
297
+ />
298
+ </>
299
+ )}
300
+ </div>
301
+ );
302
+ }
303
+
304
+ function ComputerToolbar(props: {
305
+ sessions: ComputerSession[];
306
+ relevantSessionIds: Set<string>;
307
+ selectedSessionId: string | null;
308
+ creating: boolean;
309
+ refreshing: boolean;
310
+ onSelect: (id: string) => void;
311
+ onFollow: () => void;
312
+ onCreate: () => void;
313
+ onRefresh: () => void;
314
+ }) {
315
+ const detailsRef = useRef<HTMLDetailsElement | null>(null);
316
+ const selected = props.sessions.find((session) => session.id === props.selectedSessionId);
317
+ const current = props.sessions.filter((session) => props.relevantSessionIds.has(session.id));
318
+ const others = props.sessions.filter((session) => !props.relevantSessionIds.has(session.id));
319
+ const choose = (id: string) => {
320
+ props.onSelect(id);
321
+ detailsRef.current?.removeAttribute("open");
322
+ };
323
+ return (
324
+ <div className="flex h-10 shrink-0 items-center gap-1.5 border-b border-og-border bg-og-surface-0 px-2">
325
+ <details ref={detailsRef} className="relative min-w-0">
326
+ <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">
327
+ <MonitorIcon className="size-3.5 shrink-0 text-og-muted" />
328
+ <span className="truncate font-medium">{selected?.name ?? "Computer"}</span>
329
+ <ChevronDownIcon className="size-3 shrink-0 text-og-subtle" />
330
+ </summary>
331
+ <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">
332
+ <ComputerSessionGroup
333
+ label="Current agent"
334
+ sessions={current}
335
+ selectedId={props.selectedSessionId}
336
+ onSelect={choose}
337
+ />
338
+ <ComputerSessionGroup
339
+ label="Other agents"
340
+ sessions={others}
341
+ selectedId={props.selectedSessionId}
342
+ onSelect={choose}
343
+ />
344
+ <div className="mt-1 flex gap-1 border-t border-og-border pt-1">
345
+ {current.length > 0 ? (
346
+ <MenuButton onClick={props.onFollow}>Follow agent</MenuButton>
347
+ ) : null}
348
+ <MenuButton onClick={props.onCreate} disabled={props.creating}>
349
+ <PlusIcon className="size-3.5" /> New computer
350
+ </MenuButton>
351
+ </div>
352
+ </div>
353
+ </details>
354
+ <span className="min-w-0 flex-1 truncate text-og-xs text-og-subtle">
355
+ {selected ? `${platformLabel(selected)} · ${placementLabel(selected)}` : ""}
356
+ </span>
357
+ <button
358
+ type="button"
359
+ onClick={props.onRefresh}
360
+ className="grid size-7 place-items-center rounded-og-sm text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg"
361
+ aria-label="Refresh computers"
362
+ >
363
+ <RefreshCwIcon className={cn("size-3.5", props.refreshing && "animate-spin")} />
364
+ </button>
365
+ <button
366
+ type="button"
367
+ onClick={props.onCreate}
368
+ disabled={props.creating}
369
+ 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"
370
+ aria-label="Open a new computer"
371
+ >
372
+ {props.creating ? (
373
+ <LoaderCircleIcon className="size-3.5 animate-spin" />
374
+ ) : (
375
+ <PlusIcon className="size-3.5" />
376
+ )}
377
+ </button>
378
+ </div>
379
+ );
380
+ }
381
+
382
+ function ComputerSessionGroup(props: {
383
+ label: string;
384
+ sessions: ComputerSession[];
385
+ selectedId: string | null;
386
+ onSelect: (id: string) => void;
387
+ }) {
388
+ if (props.sessions.length === 0) return null;
389
+ return (
390
+ <div className="py-1">
391
+ <p className="px-2 pb-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-subtle">
392
+ {props.label}
393
+ </p>
394
+ {props.sessions.map((session) => (
395
+ <button
396
+ key={session.id}
397
+ type="button"
398
+ onClick={() => props.onSelect(session.id)}
399
+ className={cn(
400
+ "flex w-full items-center gap-2 rounded-og-sm px-2 py-1.5 text-left transition hover:bg-og-surface-2",
401
+ session.id === props.selectedId && "bg-og-surface-2",
402
+ )}
403
+ >
404
+ <span
405
+ className={cn(
406
+ "size-1.5 rounded-full",
407
+ session.lifecycle === "active" ? "bg-og-status-running" : "bg-og-muted",
408
+ )}
409
+ />
410
+ <span className="min-w-0 flex-1">
411
+ <span className="block truncate text-og-control text-og-fg">{session.name}</span>
412
+ <span className="block truncate text-og-xs text-og-subtle">
413
+ {platformLabel(session)} · {placementLabel(session)}
414
+ </span>
415
+ </span>
416
+ </button>
417
+ ))}
418
+ </div>
419
+ );
420
+ }
421
+
422
+ function MenuButton(props: { children: ReactNode; onClick: () => void; disabled?: boolean }) {
423
+ return (
424
+ <button
425
+ type="button"
426
+ onClick={props.onClick}
427
+ disabled={props.disabled}
428
+ 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"
429
+ >
430
+ {props.children}
431
+ </button>
432
+ );
433
+ }
434
+
435
+ function ComputerTargetRail(props: {
436
+ targets: ComputerTarget[];
437
+ selectedTargetId: string | null;
438
+ loading: boolean;
439
+ onSelect: (id: string) => void;
440
+ }) {
441
+ return (
442
+ <div
443
+ className="flex h-10 shrink-0 items-center gap-1 overflow-x-auto border-b border-og-border bg-og-surface-0 px-2"
444
+ aria-label="Computer views"
445
+ >
446
+ {props.targets.map((target) => (
447
+ <button
448
+ key={target.id}
449
+ type="button"
450
+ onClick={() => props.onSelect(target.id)}
451
+ className={cn(
452
+ "flex h-7 max-w-56 shrink-0 items-center gap-1.5 rounded-og-sm border px-2 text-og-control transition",
453
+ target.id === props.selectedTargetId
454
+ ? "border-og-border-strong bg-og-surface-2 text-og-fg"
455
+ : "border-transparent text-og-muted hover:bg-og-surface-1 hover:text-og-fg",
456
+ )}
457
+ aria-pressed={target.id === props.selectedTargetId}
458
+ >
459
+ {target.kind === "screen" ? (
460
+ <MonitorIcon className="size-3.5 shrink-0" />
461
+ ) : (
462
+ <PanelsTopLeftIcon className="size-3.5 shrink-0" />
463
+ )}
464
+ <span className="truncate">{target.title || target.applicationId || target.kind}</span>
465
+ {target.focused ? (
466
+ <span
467
+ className="size-1.5 shrink-0 rounded-full bg-og-status-running"
468
+ aria-label="Focused"
469
+ />
470
+ ) : null}
471
+ </button>
472
+ ))}
473
+ {props.loading ? (
474
+ <LoaderCircleIcon className="ml-1 size-3.5 animate-spin text-og-muted" />
475
+ ) : null}
476
+ {!props.loading && props.targets.length === 0 ? (
477
+ <span className="text-og-xs text-og-subtle">Waiting for apps and screens…</span>
478
+ ) : null}
479
+ </div>
480
+ );
481
+ }
482
+
483
+ function ComputerLifecyclePanel(props: {
484
+ session: ComputerSession;
485
+ onRefresh: () => Promise<void>;
486
+ }) {
487
+ const failed = ["failed", "lost", "repair_required"].includes(props.session.lifecycle);
488
+ return (
489
+ <div className="grid min-h-0 flex-1 place-items-center bg-og-bg p-6">
490
+ <div className="max-w-sm text-center">
491
+ {failed ? (
492
+ <CircleAlertIcon className="mx-auto size-5 text-og-status-error" />
493
+ ) : (
494
+ <LoaderCircleIcon className="mx-auto size-5 animate-spin text-og-muted" />
495
+ )}
496
+ <p className="mt-3 text-og-menu font-medium text-og-fg">
497
+ {failed ? "Computer needs attention" : lifecycleLabel(props.session.lifecycle)}
498
+ </p>
499
+ <p className="mt-1 text-og-control leading-5 text-og-muted">
500
+ {props.session.failureCode ??
501
+ "The computer is being prepared on its placement. It will appear here when ready."}
502
+ </p>
503
+ {failed ? (
504
+ <button
505
+ type="button"
506
+ onClick={() => void props.onRefresh()}
507
+ className="mt-4 inline-flex h-8 items-center gap-1.5 rounded-og-sm border border-og-border bg-og-surface-1 px-3 text-og-control font-medium text-og-fg transition hover:bg-og-surface-2"
508
+ >
509
+ <RotateCcwIcon className="size-3.5" /> Check again
510
+ </button>
511
+ ) : null}
512
+ </div>
513
+ </div>
514
+ );
515
+ }
516
+
517
+ function ComputerViewport(props: {
518
+ frame: ComputerFrame | null;
519
+ observation: ComputerObservation | null;
520
+ target: ComputerTarget | null;
521
+ machineLocked: boolean;
522
+ connectionState: string;
523
+ connectionError: Error | null;
524
+ mutating: boolean;
525
+ onAction: (action: ComputerAction, frame: ComputerFrame | null) => Promise<void>;
526
+ onReconnect: () => void;
527
+ onError: (cause: unknown) => void;
528
+ }) {
529
+ const canvasRef = useRef<HTMLCanvasElement | null>(null);
530
+ const inputRef = useRef<HTMLTextAreaElement | null>(null);
531
+ const pointerStartRef = useRef<PointerStart | null>(null);
532
+ const clickTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
533
+ const lastClickRef = useRef<{
534
+ at: number;
535
+ x: number;
536
+ y: number;
537
+ frame: ComputerFrame;
538
+ } | null>(null);
539
+ const wheelRef = useRef<{
540
+ x: number;
541
+ y: number;
542
+ deltaX: number;
543
+ deltaY: number;
544
+ frame: ComputerFrame;
545
+ timer: ReturnType<typeof setTimeout> | null;
546
+ } | null>(null);
547
+ const actionRef = useRef(props.onAction);
548
+ const errorRef = useRef(props.onError);
549
+ const actionTailRef = useRef<Promise<void>>(Promise.resolve());
550
+ actionRef.current = props.onAction;
551
+ errorRef.current = props.onError;
552
+
553
+ useEffect(() => {
554
+ const frame = props.frame;
555
+ const canvas = canvasRef.current;
556
+ if (!frame || !canvas) return;
557
+ let cancelled = false;
558
+ let objectUrl: string | null = null;
559
+ void (async () => {
560
+ try {
561
+ const blob = new Blob([frame.data.slice().buffer], { type: frame.mediaType });
562
+ if (typeof createImageBitmap === "function") {
563
+ const bitmap = await createImageBitmap(blob);
564
+ if (cancelled) {
565
+ bitmap.close();
566
+ return;
567
+ }
568
+ paintCanvas(canvas, bitmap, frame.width, frame.height);
569
+ bitmap.close();
570
+ return;
571
+ }
572
+ objectUrl = URL.createObjectURL(blob);
573
+ const image = await loadImage(objectUrl);
574
+ if (!cancelled) paintCanvas(canvas, image, frame.width, frame.height);
575
+ } catch (cause) {
576
+ if (!cancelled) errorRef.current(cause);
577
+ }
578
+ })();
579
+ return () => {
580
+ cancelled = true;
581
+ if (objectUrl) URL.revokeObjectURL(objectUrl);
582
+ };
583
+ }, [props.frame]);
584
+
585
+ useEffect(
586
+ () => () => {
587
+ if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
588
+ if (wheelRef.current?.timer) clearTimeout(wheelRef.current.timer);
589
+ },
590
+ [],
591
+ );
592
+
593
+ const enqueue = useCallback((action: ComputerAction, frame: ComputerFrame | null) => {
594
+ actionTailRef.current = actionTailRef.current
595
+ .catch(() => undefined)
596
+ .then(async () => await actionRef.current(action, frame))
597
+ .catch((cause) => errorRef.current(cause));
598
+ }, []);
599
+
600
+ const point = useCallback(
601
+ (frame: ComputerFrame, clientX: number, clientY: number) =>
602
+ computerPoint(canvasRef.current, frame, clientX, clientY),
603
+ [],
604
+ );
605
+
606
+ const pointerDown = (event: PointerEvent<HTMLCanvasElement>) => {
607
+ if (!props.frame || props.mutating) return;
608
+ pointerStartRef.current = {
609
+ x: event.clientX,
610
+ y: event.clientY,
611
+ pointerId: event.pointerId,
612
+ frame: props.frame,
613
+ };
614
+ event.currentTarget.setPointerCapture?.(event.pointerId);
615
+ inputRef.current?.focus({ preventScroll: true });
616
+ };
617
+
618
+ const pointerUp = (event: PointerEvent<HTMLCanvasElement>) => {
619
+ const start = pointerStartRef.current;
620
+ pointerStartRef.current = null;
621
+ if (!start || start.pointerId !== event.pointerId) return;
622
+ const from = point(start.frame, start.x, start.y);
623
+ const to = point(start.frame, event.clientX, event.clientY);
624
+ if (!from || !to) return;
625
+ if (Math.hypot(event.clientX - start.x, event.clientY - start.y) > 5) {
626
+ enqueue(
627
+ {
628
+ type: "pointer",
629
+ frameId: start.frame.frameId,
630
+ action: "drag",
631
+ x: from.x,
632
+ y: from.y,
633
+ endX: to.x,
634
+ endY: to.y,
635
+ },
636
+ start.frame,
637
+ );
638
+ return;
639
+ }
640
+ const now = Date.now();
641
+ const previous = lastClickRef.current;
642
+ if (
643
+ previous &&
644
+ now - previous.at < 280 &&
645
+ Math.hypot(previous.x - to.x, previous.y - to.y) < 6 &&
646
+ sameFrameFence(previous.frame, start.frame)
647
+ ) {
648
+ if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
649
+ clickTimerRef.current = null;
650
+ lastClickRef.current = null;
651
+ enqueue(
652
+ {
653
+ type: "pointer",
654
+ frameId: start.frame.frameId,
655
+ action: "double_click",
656
+ x: to.x,
657
+ y: to.y,
658
+ },
659
+ start.frame,
660
+ );
661
+ return;
662
+ }
663
+ lastClickRef.current = { at: now, x: to.x, y: to.y, frame: start.frame };
664
+ clickTimerRef.current = setTimeout(() => {
665
+ clickTimerRef.current = null;
666
+ lastClickRef.current = null;
667
+ enqueue(
668
+ {
669
+ type: "pointer",
670
+ frameId: start.frame.frameId,
671
+ action: "click",
672
+ x: to.x,
673
+ y: to.y,
674
+ },
675
+ start.frame,
676
+ );
677
+ }, 280);
678
+ };
679
+
680
+ const contextMenu = (event: MouseEvent<HTMLCanvasElement>) => {
681
+ event.preventDefault();
682
+ const frame = props.frame;
683
+ if (!frame) return;
684
+ const at = point(frame, event.clientX, event.clientY);
685
+ if (at) {
686
+ enqueue(
687
+ {
688
+ type: "pointer",
689
+ frameId: frame.frameId,
690
+ action: "click",
691
+ x: at.x,
692
+ y: at.y,
693
+ button: "right",
694
+ },
695
+ frame,
696
+ );
697
+ }
698
+ };
699
+
700
+ const wheel = (event: WheelEvent<HTMLCanvasElement>) => {
701
+ const frame = props.frame;
702
+ if (!frame) return;
703
+ const at = point(frame, event.clientX, event.clientY);
704
+ if (!at) return;
705
+ event.preventDefault();
706
+ const pending = wheelRef.current;
707
+ if (pending?.timer) clearTimeout(pending.timer);
708
+ wheelRef.current = {
709
+ x: at.x,
710
+ y: at.y,
711
+ deltaX: (pending && sameFrameFence(pending.frame, frame) ? pending.deltaX : 0) + event.deltaX,
712
+ deltaY: (pending && sameFrameFence(pending.frame, frame) ? pending.deltaY : 0) + event.deltaY,
713
+ frame,
714
+ timer: setTimeout(() => {
715
+ const batch = wheelRef.current;
716
+ wheelRef.current = null;
717
+ if (batch) {
718
+ enqueue(
719
+ {
720
+ type: "pointer",
721
+ frameId: batch.frame.frameId,
722
+ action: "scroll",
723
+ x: batch.x,
724
+ y: batch.y,
725
+ deltaX: batch.deltaX,
726
+ deltaY: batch.deltaY,
727
+ },
728
+ batch.frame,
729
+ );
730
+ }
731
+ }, 45),
732
+ };
733
+ };
734
+
735
+ const keyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
736
+ const key = computerKey(event);
737
+ if (!key) return;
738
+ event.preventDefault();
739
+ enqueue({ type: "keyboard", action: "press", value: key }, null);
740
+ };
741
+
742
+ const input = (value: string) => {
743
+ if (!value) return;
744
+ enqueue({ type: "keyboard", action: "type", value }, null);
745
+ if (inputRef.current) inputRef.current.value = "";
746
+ };
747
+
748
+ const showCanvas = props.frame !== null && !props.machineLocked;
749
+ return (
750
+ <div className="relative min-h-0 flex-1 overflow-hidden bg-black">
751
+ <canvas
752
+ ref={canvasRef}
753
+ className={cn(
754
+ "absolute inset-0 m-auto max-h-full max-w-full touch-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent",
755
+ !showCanvas && "invisible",
756
+ )}
757
+ onPointerDown={pointerDown}
758
+ onPointerUp={pointerUp}
759
+ onPointerCancel={() => {
760
+ pointerStartRef.current = null;
761
+ }}
762
+ onContextMenu={contextMenu}
763
+ onWheel={wheel}
764
+ aria-label={`Interactive ${props.target?.kind ?? "computer"} view`}
765
+ />
766
+ <textarea
767
+ ref={inputRef}
768
+ defaultValue=""
769
+ onInput={(event) => input(event.currentTarget.value)}
770
+ onKeyDown={keyDown}
771
+ className="pointer-events-none absolute left-1/2 top-1/2 size-px resize-none overflow-hidden opacity-0"
772
+ aria-label="Computer keyboard input"
773
+ autoCapitalize="off"
774
+ autoCorrect="off"
775
+ spellCheck={false}
776
+ />
777
+ {!showCanvas ? (
778
+ <ComputerViewportFallback
779
+ observation={props.observation}
780
+ machineLocked={props.machineLocked}
781
+ connectionState={props.connectionState}
782
+ error={props.connectionError}
783
+ onAction={(action) => enqueue(action, null)}
784
+ onReconnect={props.onReconnect}
785
+ />
786
+ ) : null}
787
+ {props.mutating ? (
788
+ <div className="pointer-events-none absolute right-3 top-3 flex items-center gap-1.5 rounded-full border border-white/10 bg-black/65 px-2.5 py-1 text-[11px] text-white/80 backdrop-blur">
789
+ <LoaderCircleIcon className="size-3 animate-spin" /> Acting
790
+ </div>
791
+ ) : null}
792
+ </div>
793
+ );
794
+ }
795
+
796
+ function ComputerViewportFallback(props: {
797
+ observation: ComputerObservation | null;
798
+ machineLocked: boolean;
799
+ connectionState: string;
800
+ error: Error | null;
801
+ onAction: (action: ComputerAction) => void;
802
+ onReconnect: () => void;
803
+ }) {
804
+ if (props.machineLocked) {
805
+ return (
806
+ <div className="absolute inset-0 grid place-items-center bg-og-bg p-6">
807
+ <div className="max-w-sm text-center">
808
+ <LockKeyholeIcon className="mx-auto size-6 text-og-muted" />
809
+ <p className="mt-3 text-og-menu font-medium text-og-fg">Machine locked</p>
810
+ <p className="mt-1 text-og-control leading-5 text-og-muted">
811
+ Live frames and input stay private until someone unlocks the machine.
812
+ </p>
813
+ </div>
814
+ </div>
815
+ );
816
+ }
817
+ const interactive = semanticNodes(props.observation)
818
+ .filter((node) => semanticAction(node) !== null)
819
+ .slice(0, 10);
820
+ return (
821
+ <div className="absolute inset-0 grid place-items-center bg-og-bg p-6">
822
+ <div className="w-full max-w-md rounded-og-lg border border-og-border bg-og-surface-0 p-4 shadow-lg">
823
+ <div className="flex items-center gap-2">
824
+ {props.error ? (
825
+ <CircleAlertIcon className="size-4 text-og-status-error" />
826
+ ) : (
827
+ <LoaderCircleIcon className="size-4 animate-spin text-og-muted" />
828
+ )}
829
+ <p className="text-og-menu font-medium text-og-fg">
830
+ {props.error
831
+ ? "Live view disconnected"
832
+ : computerConnectionLabel(props.connectionState)}
833
+ </p>
834
+ </div>
835
+ {interactive.length > 0 ? (
836
+ <div className="mt-3 border-t border-og-border pt-3">
837
+ <p className="mb-2 text-og-xs text-og-subtle">Native controls remain available</p>
838
+ <div className="flex flex-wrap gap-1.5">
839
+ {interactive.map((node) => (
840
+ <button
841
+ key={node.ref}
842
+ type="button"
843
+ onClick={() => {
844
+ const action = semanticAction(node);
845
+ if (action) props.onAction(action);
846
+ }}
847
+ className="rounded-og-sm border border-og-border bg-og-surface-1 px-2 py-1 text-og-control text-og-fg transition hover:bg-og-surface-2"
848
+ >
849
+ {node.name || node.role}
850
+ </button>
851
+ ))}
852
+ </div>
853
+ </div>
854
+ ) : null}
855
+ {props.error ? (
856
+ <button
857
+ type="button"
858
+ onClick={props.onReconnect}
859
+ className="mt-3 text-og-control font-medium text-og-accent hover:underline"
860
+ >
861
+ Reconnect
862
+ </button>
863
+ ) : null}
864
+ </div>
865
+ </div>
866
+ );
867
+ }
868
+
869
+ function ComputerSemanticPanel(props: {
870
+ observation: ComputerObservation | null;
871
+ mutating: boolean;
872
+ onAction: (action: ComputerAction) => void;
873
+ }) {
874
+ const nodes = semanticNodes(props.observation).slice(0, 100);
875
+ return (
876
+ <aside className="w-64 shrink-0 overflow-y-auto border-l border-og-border bg-og-surface-0 p-2">
877
+ <div className="mb-2 flex items-center gap-1.5 px-1 text-og-xs font-medium uppercase tracking-[0.1em] text-og-subtle">
878
+ <KeyboardIcon className="size-3" /> Native controls
879
+ </div>
880
+ {nodes.length === 0 ? (
881
+ <p className="px-1 py-2 text-og-control leading-5 text-og-muted">
882
+ This view has no accessibility controls. Use the live image instead.
883
+ </p>
884
+ ) : (
885
+ <div className="space-y-0.5">
886
+ {nodes.map((node) => {
887
+ const action = semanticAction(node);
888
+ return (
889
+ <button
890
+ key={node.ref}
891
+ type="button"
892
+ disabled={!action || props.mutating}
893
+ onClick={() => {
894
+ if (action) props.onAction(action);
895
+ }}
896
+ 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"
897
+ >
898
+ <span className="mt-0.5 text-[10px] uppercase text-og-subtle">{node.role}</span>
899
+ <span className="min-w-0 flex-1 truncate text-og-control text-og-fg">
900
+ {node.name || semanticValue(node) || node.identifier || "Unnamed control"}
901
+ </span>
902
+ </button>
903
+ );
904
+ })}
905
+ </div>
906
+ )}
907
+ </aside>
908
+ );
909
+ }
910
+
911
+ function ComputerStatusBar(props: {
912
+ session: ComputerSession | null;
913
+ target: ComputerTarget | null;
914
+ connectionState: string;
915
+ refreshing: boolean;
916
+ showControls: boolean;
917
+ controlCount: number;
918
+ onToggleControls: () => void;
919
+ }) {
920
+ const screen = props.target?.kind === "screen";
921
+ return (
922
+ <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">
923
+ <span
924
+ className={cn(
925
+ "size-1.5 rounded-full",
926
+ props.connectionState === "live" ? "bg-og-status-running" : "bg-og-muted",
927
+ )}
928
+ />
929
+ <span>
930
+ {props.connectionState === "live" ? "Live" : computerConnectionLabel(props.connectionState)}
931
+ </span>
932
+ <span className="min-w-0 flex-1 truncate">
933
+ {screen
934
+ ? "Full screen · input may move pointer and focus"
935
+ : props.session?.capabilities?.backgroundActions
936
+ ? "Window · native actions can stay in the background"
937
+ : (props.target?.kind ?? "Computer")}
938
+ </span>
939
+ {screen ? <MousePointer2Icon className="size-3" aria-hidden /> : null}
940
+ {props.refreshing ? <LoaderCircleIcon className="size-3 animate-spin" /> : null}
941
+ <button
942
+ type="button"
943
+ onClick={props.onToggleControls}
944
+ aria-pressed={props.showControls}
945
+ className={cn(
946
+ "rounded px-1.5 py-0.5 transition hover:bg-og-surface-2 hover:text-og-fg",
947
+ props.showControls && "bg-og-surface-2 text-og-fg",
948
+ )}
949
+ >
950
+ Controls {props.controlCount}
951
+ </button>
952
+ </div>
953
+ );
954
+ }
955
+
956
+ function ComputerNotice(props: { icon: ReactNode; text: string; className?: string }) {
957
+ return (
958
+ <div
959
+ className={cn(
960
+ "grid h-full place-items-center bg-og-bg text-og-control text-og-muted",
961
+ props.className,
962
+ )}
963
+ >
964
+ <div className="flex items-center gap-2">
965
+ {props.icon}
966
+ {props.text}
967
+ </div>
968
+ </div>
969
+ );
970
+ }
971
+
972
+ function semanticNodes(observation: ComputerObservation | null): InteractionSemanticNode[] {
973
+ if (observation?.semantic?.kind !== "snapshot") return [];
974
+ const result: InteractionSemanticNode[] = [];
975
+ const visit = (node: InteractionSemanticNode) => {
976
+ result.push(node);
977
+ node.children?.forEach(visit);
978
+ };
979
+ observation.semantic.roots.forEach(visit);
980
+ return result;
981
+ }
982
+
983
+ function semanticAction(node: InteractionSemanticNode): ComputerAction | null {
984
+ const invoke = ["invoke", "press", "click"].some((action) => node.actions.includes(action));
985
+ if (invoke) {
986
+ return {
987
+ type: "semantic",
988
+ locator: { kind: "ref", ref: node.ref },
989
+ action: "invoke",
990
+ };
991
+ }
992
+ if (node.actions.includes("focus")) {
993
+ return {
994
+ type: "semantic",
995
+ locator: { kind: "ref", ref: node.ref },
996
+ action: "focus",
997
+ };
998
+ }
999
+ return null;
1000
+ }
1001
+
1002
+ function semanticValue(node: InteractionSemanticNode): string | null {
1003
+ return typeof node.value === "string" ? node.value : null;
1004
+ }
1005
+
1006
+ function paintCanvas(
1007
+ canvas: HTMLCanvasElement,
1008
+ image: CanvasImageSource,
1009
+ width: number,
1010
+ height: number,
1011
+ ): void {
1012
+ canvas.width = width;
1013
+ canvas.height = height;
1014
+ const context = canvas.getContext("2d", { alpha: false });
1015
+ if (!context) throw new Error("Computer canvas is unavailable.");
1016
+ context.drawImage(image, 0, 0, width, height);
1017
+ }
1018
+
1019
+ function loadImage(url: string): Promise<HTMLImageElement> {
1020
+ return new Promise((resolve, reject) => {
1021
+ const image = new Image();
1022
+ image.onload = () => resolve(image);
1023
+ image.onerror = () => reject(new Error("Computer frame image could not be decoded."));
1024
+ image.src = url;
1025
+ });
1026
+ }
1027
+
1028
+ function computerPoint(
1029
+ canvas: HTMLCanvasElement | null,
1030
+ frame: ComputerFrame,
1031
+ clientX: number,
1032
+ clientY: number,
1033
+ ): { x: number; y: number } | null {
1034
+ if (!canvas) return null;
1035
+ const rect = canvas.getBoundingClientRect();
1036
+ if (rect.width <= 0 || rect.height <= 0) return null;
1037
+ return {
1038
+ x: Math.max(0, Math.min(frame.width - 1, ((clientX - rect.left) / rect.width) * frame.width)),
1039
+ y: Math.max(0, Math.min(frame.height - 1, ((clientY - rect.top) / rect.height) * frame.height)),
1040
+ };
1041
+ }
1042
+
1043
+ function computerKey(event: KeyboardEvent<HTMLTextAreaElement>): string | null {
1044
+ const special = new Set([
1045
+ "Enter",
1046
+ "Tab",
1047
+ "Escape",
1048
+ "Backspace",
1049
+ "Delete",
1050
+ "ArrowUp",
1051
+ "ArrowDown",
1052
+ "ArrowLeft",
1053
+ "ArrowRight",
1054
+ "Home",
1055
+ "End",
1056
+ "PageUp",
1057
+ "PageDown",
1058
+ ]);
1059
+ const modified = event.altKey || event.ctrlKey || event.metaKey;
1060
+ if (!modified && !special.has(event.key)) return null;
1061
+ const parts: string[] = [];
1062
+ if (event.ctrlKey) parts.push("Control");
1063
+ if (event.altKey) parts.push("Alt");
1064
+ if (event.metaKey) parts.push("Meta");
1065
+ if (event.shiftKey) parts.push("Shift");
1066
+ parts.push(event.key === " " ? "Space" : event.key);
1067
+ return parts.join("+");
1068
+ }
1069
+
1070
+ function sameFrameFence(left: ComputerFrame, right: ComputerFrame): boolean {
1071
+ return (
1072
+ left.frameId === right.frameId &&
1073
+ left.computerSessionId === right.computerSessionId &&
1074
+ left.targetId === right.targetId &&
1075
+ left.targetGeneration === right.targetGeneration
1076
+ );
1077
+ }
1078
+
1079
+ function isLiveComputer(session: ComputerSession): boolean {
1080
+ return !["ending", "ended", "failed", "lost"].includes(session.lifecycle);
1081
+ }
1082
+
1083
+ function platformLabel(session: ComputerSession): string {
1084
+ switch (session.platform) {
1085
+ case "macos":
1086
+ return "Mac";
1087
+ case "linux":
1088
+ return "Linux";
1089
+ case "windows":
1090
+ return "Windows";
1091
+ default:
1092
+ return "Computer";
1093
+ }
1094
+ }
1095
+
1096
+ function placementLabel(session: ComputerSession): string {
1097
+ switch (session.placement.kind) {
1098
+ case "sandbox_group":
1099
+ return "Agent sandbox";
1100
+ case "connected_machine":
1101
+ return "Connected machine";
1102
+ case "attached_device":
1103
+ return "Your machine";
1104
+ case "external_provider":
1105
+ return "Cloud computer";
1106
+ }
1107
+ }
1108
+
1109
+ function lifecycleLabel(lifecycle: ComputerSession["lifecycle"]): string {
1110
+ switch (lifecycle) {
1111
+ case "starting":
1112
+ return "Opening computer…";
1113
+ case "suspending":
1114
+ return "Pausing computer…";
1115
+ case "suspended":
1116
+ return "Computer paused";
1117
+ case "restoring":
1118
+ return "Restoring computer…";
1119
+ case "ending":
1120
+ return "Closing computer…";
1121
+ default:
1122
+ return "Connecting to computer…";
1123
+ }
1124
+ }
1125
+
1126
+ function computerConnectionLabel(state: string): string {
1127
+ switch (state) {
1128
+ case "attaching":
1129
+ return "Opening computer view…";
1130
+ case "connecting":
1131
+ return "Connecting…";
1132
+ case "reconnecting":
1133
+ return "Reconnecting…";
1134
+ case "error":
1135
+ return "Disconnected";
1136
+ default:
1137
+ return "Waiting for computer…";
1138
+ }
1139
+ }