@opengeni/react 0.48.0 → 0.57.0

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 (325) hide show
  1. package/README.md +138 -2
  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 +199 -0
  13. package/dist/artifacts.js.map +1 -0
  14. package/dist/{chunk-NJ7M2B6Z.js → chunk-5UQPNLIO.js} +16 -8
  15. package/dist/{chunk-NJ7M2B6Z.js.map → chunk-5UQPNLIO.js.map} +1 -1
  16. package/dist/{chunk-LHLYWG5R.js → chunk-6KLX2RH5.js} +461 -38
  17. package/dist/chunk-6KLX2RH5.js.map +1 -0
  18. package/dist/chunk-7WCLAEFV.js +2828 -0
  19. package/dist/chunk-7WCLAEFV.js.map +1 -0
  20. package/dist/{chunk-I4MPFDRP.js → chunk-B6WPZSTT.js} +104 -104
  21. package/dist/{chunk-I4MPFDRP.js.map → chunk-B6WPZSTT.js.map} +1 -1
  22. package/dist/{chunk-EKZH6IDG.js → chunk-C5ZPSSS5.js} +111 -1
  23. package/dist/chunk-C5ZPSSS5.js.map +1 -0
  24. package/dist/{chunk-OL6QVXGF.js → chunk-HA46CYQK.js} +152 -8
  25. package/dist/chunk-HA46CYQK.js.map +1 -0
  26. package/dist/chunk-HUIKIDM5.js +2148 -0
  27. package/dist/chunk-HUIKIDM5.js.map +1 -0
  28. package/dist/{chunk-FWXGSNXT.js → chunk-IGC6A2ZI.js} +585 -34
  29. package/dist/chunk-IGC6A2ZI.js.map +1 -0
  30. package/dist/chunk-JVYQRRC4.js +53 -0
  31. package/dist/chunk-JVYQRRC4.js.map +1 -0
  32. package/dist/chunk-KCGXPP5V.js +157 -0
  33. package/dist/chunk-KCGXPP5V.js.map +1 -0
  34. package/dist/chunk-LLOLUHVG.js +5937 -0
  35. package/dist/chunk-LLOLUHVG.js.map +1 -0
  36. package/dist/{chunk-WYA65Y3F.js → chunk-MKPIAGVL.js} +13 -3
  37. package/dist/chunk-MKPIAGVL.js.map +1 -0
  38. package/dist/chunk-PEI6RANI.js +3234 -0
  39. package/dist/chunk-PEI6RANI.js.map +1 -0
  40. package/dist/chunk-R3AZOAB5.js +933 -0
  41. package/dist/chunk-R3AZOAB5.js.map +1 -0
  42. package/dist/{chunk-O4VXOBGF.js → chunk-WSK7G5LE.js} +70 -2
  43. package/dist/chunk-WSK7G5LE.js.map +1 -0
  44. package/dist/{chunk-23EJ676W.js → chunk-XBZWOM2K.js} +9 -4
  45. package/dist/chunk-XBZWOM2K.js.map +1 -0
  46. package/dist/{chunk-XCNTXWBB.js → chunk-Y7OEDUJ5.js} +856 -338
  47. package/dist/chunk-Y7OEDUJ5.js.map +1 -0
  48. package/dist/client.d.ts +10 -2
  49. package/dist/clipboard.d.ts +1 -0
  50. package/dist/clipboard.js +7 -0
  51. package/dist/clipboard.js.map +1 -0
  52. package/dist/commands/index.d.ts +3 -3
  53. package/dist/commands/registry.d.ts +1 -1
  54. package/dist/commands/types.d.ts +1 -1
  55. package/dist/components/approval-surface.d.ts +2 -2
  56. package/dist/components/artifacts/artifact-surface.d.ts +18 -0
  57. package/dist/components/artifacts/document-editor.d.ts +172 -0
  58. package/dist/components/artifacts/editable-artifact-ui.d.ts +13 -0
  59. package/dist/components/artifacts/editable-artifact-workbench.d.ts +49 -0
  60. package/dist/components/artifacts/editable-document.d.ts +24 -0
  61. package/dist/components/artifacts/editable-presentation.d.ts +24 -0
  62. package/dist/components/artifacts/editable-spreadsheet.d.ts +32 -0
  63. package/dist/components/artifacts/index.d.ts +9 -0
  64. package/dist/components/artifacts/presentation-editor.d.ts +297 -0
  65. package/dist/components/artifacts/published-html-artifact-frame.d.ts +14 -0
  66. package/dist/components/artifacts/spreadsheet-canvas.d.ts +120 -0
  67. package/dist/components/artifacts/spreadsheet-grid.d.ts +113 -0
  68. package/dist/components/browser-input.d.ts +7 -0
  69. package/dist/components/browser-viewer.d.ts +30 -0
  70. package/dist/components/chat-composer.d.ts +8 -8
  71. package/dist/components/code-editor.d.ts +25 -3
  72. package/dist/components/command-palette.d.ts +2 -2
  73. package/dist/components/composer-transcription-control.d.ts +2 -2
  74. package/dist/components/composer.d.ts +17 -17
  75. package/dist/components/computer-viewer.d.ts +24 -0
  76. package/dist/components/copy-button.d.ts +2 -2
  77. package/dist/components/desktop-viewer.d.ts +50 -22
  78. package/dist/components/diff-view.d.ts +1 -1
  79. package/dist/components/enrollment-consent.d.ts +1 -1
  80. package/dist/components/enrollment-device-flow.d.ts +1 -1
  81. package/dist/components/file-browser.d.ts +2 -2
  82. package/dist/components/fleet-tile.d.ts +1 -1
  83. package/dist/components/generated-video-player.d.ts +10 -0
  84. package/dist/components/human-input-form.d.ts +1 -1
  85. package/dist/components/human-input-surface.d.ts +2 -2
  86. package/dist/components/interaction-intervention-banner.d.ts +11 -0
  87. package/dist/components/interactive-workbench-viewers.d.ts +2 -0
  88. package/dist/components/machine-card.d.ts +2 -2
  89. package/dist/components/machine-dock-bar.d.ts +3 -3
  90. package/dist/components/machine-health-pill.d.ts +2 -2
  91. package/dist/components/machine-input-display.d.ts +1 -1
  92. package/dist/components/machine-metrics.d.ts +2 -2
  93. package/dist/components/machine-status-pill.d.ts +4 -4
  94. package/dist/components/machines/health.d.ts +1 -1
  95. package/dist/components/machines/machine-detail.d.ts +3 -3
  96. package/dist/components/machines/metric-history-chart.d.ts +1 -1
  97. package/dist/components/machines/metric-sparkline.d.ts +2 -2
  98. package/dist/components/machines/series.d.ts +2 -2
  99. package/dist/components/machines-dashboard.d.ts +2 -2
  100. package/dist/components/markdown.d.ts +1 -1
  101. package/dist/components/message-timeline.d.ts +12 -5
  102. package/dist/components/model-picker.d.ts +2 -2
  103. package/dist/components/model-policy-picker.d.ts +7 -7
  104. package/dist/components/pierre-diff.d.ts +1 -1
  105. package/dist/components/pierre-file.d.ts +1 -1
  106. package/dist/components/queue-draft-policy.d.ts +1 -1
  107. package/dist/components/queue-surface-implementation.d.ts +3 -3
  108. package/dist/components/queue-surface-state.d.ts +4 -4
  109. package/dist/components/queue-surface.d.ts +5 -5
  110. package/dist/components/sandbox-files.d.ts +3 -3
  111. package/dist/components/sandbox-terminal.d.ts +24 -4
  112. package/dist/components/sandbox-workspace.d.ts +28 -9
  113. package/dist/components/session-chrome.d.ts +4 -4
  114. package/dist/components/session-status.d.ts +2 -2
  115. package/dist/components/timeline-annotation-selection.d.ts +9 -0
  116. package/dist/components/timeline-annotations-dialog.d.ts +16 -0
  117. package/dist/components/timeline-annotations.d.ts +13 -0
  118. package/dist/components/tooltip.d.ts +4 -4
  119. package/dist/components/user-message-body.d.ts +2 -2
  120. package/dist/components/workbench-changes.d.ts +2 -2
  121. package/dist/components/workspace-dock.d.ts +1 -1
  122. package/dist/composer.d.ts +11 -11
  123. package/dist/composer.js +7 -7
  124. package/dist/hooks/internal.d.ts +1 -1
  125. package/dist/hooks/use-attached-browsers.d.ts +21 -0
  126. package/dist/hooks/use-auth-runs.d.ts +32 -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-downloads.d.ts +25 -0
  130. package/dist/hooks/use-browser-frame-stream.d.ts +28 -0
  131. package/dist/hooks/use-browser-identities.d.ts +30 -0
  132. package/dist/hooks/use-browser-session.d.ts +29 -0
  133. package/dist/hooks/use-browser-sessions.d.ts +32 -0
  134. package/dist/hooks/use-channels.d.ts +26 -0
  135. package/dist/hooks/use-codex-accounts.d.ts +2 -2
  136. package/dist/hooks/use-composer.d.ts +36 -9
  137. package/dist/hooks/use-computer-frame-stream.d.ts +24 -0
  138. package/dist/hooks/use-computer-session.d.ts +29 -0
  139. package/dist/hooks/use-computer-sessions.d.ts +28 -0
  140. package/dist/hooks/use-desktop-stream.d.ts +5 -1
  141. package/dist/hooks/use-environments.d.ts +1 -1
  142. package/dist/hooks/use-file-attachments.d.ts +1 -1
  143. package/dist/hooks/use-goal.d.ts +2 -2
  144. package/dist/hooks/use-human-input.d.ts +2 -2
  145. package/dist/hooks/use-interaction-interventions.d.ts +27 -0
  146. package/dist/hooks/use-interaction-invalidation.d.ts +19 -0
  147. package/dist/hooks/use-last-started-turn-policy.d.ts +2 -2
  148. package/dist/hooks/use-machine-chip.d.ts +2 -2
  149. package/dist/hooks/use-machines.d.ts +4 -4
  150. package/dist/hooks/use-network-routes.d.ts +25 -0
  151. package/dist/hooks/use-packs.d.ts +7 -2
  152. package/dist/hooks/use-rigs.d.ts +1 -1
  153. package/dist/hooks/use-sandbox-files.d.ts +6 -4
  154. package/dist/hooks/use-sandbox-git.d.ts +6 -5
  155. package/dist/hooks/use-sandbox-terminal.d.ts +1 -1
  156. package/dist/hooks/use-scheduled-tasks.d.ts +1 -1
  157. package/dist/hooks/use-session-capabilities.d.ts +5 -4
  158. package/dist/hooks/use-session-control.d.ts +1 -1
  159. package/dist/hooks/use-session-events.d.ts +2 -2
  160. package/dist/hooks/use-session-lineage.d.ts +1 -1
  161. package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
  162. package/dist/hooks/use-session.d.ts +2 -2
  163. package/dist/hooks/use-site-auth-connections.d.ts +25 -0
  164. package/dist/hooks/use-slash-commands.d.ts +1 -1
  165. package/dist/hooks/use-supergrok-accounts.d.ts +26 -0
  166. package/dist/hooks/use-terminal-stream.d.ts +11 -2
  167. package/dist/hooks/use-turn-queue.d.ts +2 -2
  168. package/dist/hooks/use-video-artifact-playback.d.ts +9 -0
  169. package/dist/hooks/use-voice-input.d.ts +1 -1
  170. package/dist/hooks/use-workspace-capture.d.ts +1 -1
  171. package/dist/hooks/use-workspace-edit.d.ts +1 -1
  172. package/dist/hooks/use-workspace-sessions.d.ts +1 -1
  173. package/dist/hooks/use-workspaces.d.ts +1 -1
  174. package/dist/index.d.ts +162 -155
  175. package/dist/index.js +1822 -1703
  176. package/dist/index.js.map +1 -1
  177. package/dist/interaction.d.ts +33 -0
  178. package/dist/interaction.js +550 -0
  179. package/dist/interaction.js.map +1 -0
  180. package/dist/interactive-workbench-viewers-TOHBXJEE.js +15 -0
  181. package/dist/interactive-workbench-viewers-TOHBXJEE.js.map +1 -0
  182. package/dist/lib/host-platform.d.ts +5 -0
  183. package/dist/lib/relay-wire.d.ts +24 -1
  184. package/dist/lib/sandbox-liveness.d.ts +9 -0
  185. package/dist/lib/terminal-capability.d.ts +5 -0
  186. package/dist/lib/xterm-theme.d.ts +1 -1
  187. package/dist/machines.d.ts +30 -30
  188. package/dist/machines.js +2 -2
  189. package/dist/model-policy.d.ts +1 -1
  190. package/dist/model-policy.js +1 -1
  191. package/dist/provider.d.ts +6 -5
  192. package/dist/{queue-surface-implementation-U5JFG7HT.js → queue-surface-implementation-2KVJTYXH.js} +100 -48
  193. package/dist/queue-surface-implementation-2KVJTYXH.js.map +1 -0
  194. package/dist/realtime/dropdown-menu.d.ts +8 -8
  195. package/dist/realtime/realtime-control.d.ts +8 -8
  196. package/dist/realtime.d.ts +4 -4
  197. package/dist/realtime.js +38 -11
  198. package/dist/realtime.js.map +1 -1
  199. package/dist/session-context.d.ts +28 -2
  200. package/dist/session-ui.d.ts +14 -14
  201. package/dist/session-ui.js +5 -4
  202. package/dist/session.d.ts +28 -28
  203. package/dist/session.js +10 -10
  204. package/dist/timeline/activity-rail.d.ts +5 -3
  205. package/dist/timeline/disclosure-context.d.ts +1 -1
  206. package/dist/timeline/fleet-decision-projection.d.ts +1 -1
  207. package/dist/timeline/fleet-decision-row.d.ts +3 -3
  208. package/dist/timeline/index.d.ts +19 -19
  209. package/dist/timeline/parsers.d.ts +4 -1
  210. package/dist/timeline/projection.d.ts +3 -3
  211. package/dist/timeline/registry.d.ts +8 -2
  212. package/dist/timeline/screenshot-lightbox.d.ts +2 -2
  213. package/dist/timeline/shared.d.ts +21 -10
  214. package/dist/timeline/tool-diff.d.ts +2 -2
  215. package/dist/timeline/tool-renderers.d.ts +1 -1
  216. package/dist/timeline/turn-summary.d.ts +3 -3
  217. package/dist/timeline/types.d.ts +44 -3
  218. package/dist/timeline-annotation-selection-3L7LHGG2.js +187 -0
  219. package/dist/timeline-annotation-selection-3L7LHGG2.js.map +1 -0
  220. package/dist/timeline-annotations-dialog-U7EVLR75.js +196 -0
  221. package/dist/timeline-annotations-dialog-U7EVLR75.js.map +1 -0
  222. package/package.json +36 -7
  223. package/src/artifacts-document.ts +31 -0
  224. package/src/artifacts-presentation.ts +36 -0
  225. package/src/artifacts-spreadsheet.ts +21 -0
  226. package/src/artifacts.ts +89 -0
  227. package/src/client.ts +157 -1
  228. package/src/clipboard.ts +2 -0
  229. package/src/components/artifacts/artifact-surface.tsx +93 -0
  230. package/src/components/artifacts/document-editor.tsx +2255 -0
  231. package/src/components/artifacts/editable-artifact-ui.tsx +89 -0
  232. package/src/components/artifacts/editable-artifact-workbench.tsx +203 -0
  233. package/src/components/artifacts/editable-document.tsx +657 -0
  234. package/src/components/artifacts/editable-presentation.tsx +1020 -0
  235. package/src/components/artifacts/editable-spreadsheet.tsx +733 -0
  236. package/src/components/artifacts/index.ts +104 -0
  237. package/src/components/artifacts/presentation-editor.tsx +3148 -0
  238. package/src/components/artifacts/published-html-artifact-frame.tsx +38 -0
  239. package/src/components/artifacts/spreadsheet-canvas.ts +799 -0
  240. package/src/components/artifacts/spreadsheet-grid.tsx +2129 -0
  241. package/src/components/browser-input.ts +65 -0
  242. package/src/components/browser-viewer.tsx +2702 -0
  243. package/src/components/chat-composer.tsx +13 -0
  244. package/src/components/code-editor.tsx +89 -30
  245. package/src/components/composer-transcription-control.tsx +10 -6
  246. package/src/components/computer-viewer.tsx +1636 -0
  247. package/src/components/desktop-viewer.tsx +215 -74
  248. package/src/components/generated-video-player.tsx +89 -0
  249. package/src/components/interaction-intervention-banner.tsx +99 -0
  250. package/src/components/interactive-workbench-viewers.ts +2 -0
  251. package/src/components/machine-input-display.ts +3 -0
  252. package/src/components/machines/machine-detail.tsx +1 -1
  253. package/src/components/message-timeline.tsx +289 -32
  254. package/src/components/model-policy-picker.tsx +15 -0
  255. package/src/components/queue-surface-implementation.tsx +116 -60
  256. package/src/components/sandbox-files.tsx +2 -2
  257. package/src/components/sandbox-terminal.tsx +128 -46
  258. package/src/components/sandbox-workspace.tsx +256 -90
  259. package/src/components/session-chrome.tsx +53 -13
  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/workbench-changes.tsx +188 -31
  264. package/src/components/workspace-dock.tsx +59 -5
  265. package/src/hooks/use-attached-browsers.ts +154 -0
  266. package/src/hooks/use-auth-runs.ts +278 -0
  267. package/src/hooks/use-browser-downloads.ts +218 -0
  268. package/src/hooks/use-browser-frame-stream.ts +425 -0
  269. package/src/hooks/use-browser-identities.ts +266 -0
  270. package/src/hooks/use-browser-session.ts +543 -0
  271. package/src/hooks/use-browser-sessions.ts +286 -0
  272. package/src/hooks/use-channels.ts +99 -0
  273. package/src/hooks/use-composer.ts +769 -24
  274. package/src/hooks/use-computer-frame-stream.ts +434 -0
  275. package/src/hooks/use-computer-session.ts +462 -0
  276. package/src/hooks/use-computer-sessions.ts +251 -0
  277. package/src/hooks/use-desktop-stream.ts +39 -7
  278. package/src/hooks/use-interaction-interventions.ts +242 -0
  279. package/src/hooks/use-interaction-invalidation.ts +66 -0
  280. package/src/hooks/use-machine-chip.ts +15 -3
  281. package/src/hooks/use-machines.ts +33 -3
  282. package/src/hooks/use-network-routes.ts +214 -0
  283. package/src/hooks/use-packs.ts +51 -0
  284. package/src/hooks/use-sandbox-files.ts +98 -55
  285. package/src/hooks/use-sandbox-git.ts +93 -38
  286. package/src/hooks/use-sandbox-terminal.ts +3 -2
  287. package/src/hooks/use-session-capabilities.ts +59 -46
  288. package/src/hooks/use-site-auth-connections.ts +226 -0
  289. package/src/hooks/use-supergrok-accounts.ts +81 -0
  290. package/src/hooks/use-terminal-stream.ts +359 -47
  291. package/src/hooks/use-video-artifact-playback.ts +28 -0
  292. package/src/hooks/use-workspace-edit.ts +2 -1
  293. package/src/index.ts +18 -0
  294. package/src/interaction.ts +101 -0
  295. package/src/lib/host-platform.ts +10 -0
  296. package/src/lib/relay-wire.ts +47 -4
  297. package/src/lib/sandbox-liveness.ts +11 -0
  298. package/src/lib/terminal-capability.ts +10 -0
  299. package/src/model-policy.ts +19 -4
  300. package/src/provider.tsx +133 -1
  301. package/src/realtime/realtime-control.tsx +34 -6
  302. package/src/session-context.ts +156 -1
  303. package/src/timeline/activity-rail.tsx +25 -3
  304. package/src/timeline/index.ts +3 -0
  305. package/src/timeline/parsers.ts +14 -1
  306. package/src/timeline/projection.ts +269 -14
  307. package/src/timeline/registry.ts +13 -1
  308. package/src/timeline/screenshot-lightbox.tsx +19 -9
  309. package/src/timeline/shared.tsx +56 -8
  310. package/src/timeline/tool-renderers.tsx +303 -50
  311. package/src/timeline/types.ts +57 -3
  312. package/styles/compiled.css +312 -21
  313. package/styles/index.css +15 -0
  314. package/styles/responsive.css +13 -1
  315. package/dist/chunk-23EJ676W.js.map +0 -1
  316. package/dist/chunk-EKZH6IDG.js.map +0 -1
  317. package/dist/chunk-FWXGSNXT.js.map +0 -1
  318. package/dist/chunk-LHLYWG5R.js.map +0 -1
  319. package/dist/chunk-O4VXOBGF.js.map +0 -1
  320. package/dist/chunk-OL6QVXGF.js.map +0 -1
  321. package/dist/chunk-WYA65Y3F.js.map +0 -1
  322. package/dist/chunk-XCNTXWBB.js.map +0 -1
  323. package/dist/fleet-decision-projection-SLNZMS42.js +0 -241
  324. package/dist/fleet-decision-projection-SLNZMS42.js.map +0 -1
  325. package/dist/queue-surface-implementation-U5JFG7HT.js.map +0 -1
@@ -0,0 +1,1636 @@
1
+ import type {
2
+ ComputerAction,
3
+ ComputerClipboard,
4
+ ComputerFrame,
5
+ ComputerObservation,
6
+ ComputerSession,
7
+ ComputerTarget,
8
+ InteractionIntervention,
9
+ InteractionSemanticNode,
10
+ } from "@opengeni/sdk/interaction";
11
+ import type { DesktopStreamCapability } from "@opengeni/sdk";
12
+ import {
13
+ ChevronDownIcon,
14
+ CircleAlertIcon,
15
+ KeyboardIcon,
16
+ LoaderCircleIcon,
17
+ LockKeyholeIcon,
18
+ MonitorIcon,
19
+ MousePointer2Icon,
20
+ PanelsTopLeftIcon,
21
+ PlusIcon,
22
+ RefreshCwIcon,
23
+ RotateCcwIcon,
24
+ } from "lucide-react";
25
+ import {
26
+ type ClipboardEvent,
27
+ type KeyboardEvent,
28
+ type MouseEvent,
29
+ type PointerEvent,
30
+ type ReactNode,
31
+ type WheelEvent,
32
+ useCallback,
33
+ useEffect,
34
+ useMemo,
35
+ useRef,
36
+ useState,
37
+ } from "react";
38
+ import {
39
+ type ComputerFrameWebSocketFactory,
40
+ useComputerFrameStream,
41
+ } from "../hooks/use-computer-frame-stream";
42
+ import { useComputerSession } from "../hooks/use-computer-session";
43
+ import { useComputerSessions } from "../hooks/use-computer-sessions";
44
+ import { useInteractionInterventions } from "../hooks/use-interaction-interventions";
45
+ import { cn } from "../lib/cn";
46
+ import { copyTextToClipboard } from "../lib/clipboard";
47
+ import type { EmbeddedComputerInteractionClientOverride } from "../session-context";
48
+ import { InteractionInterventionBanner } from "./interaction-intervention-banner";
49
+ import { DesktopViewer } from "./desktop-viewer";
50
+
51
+ export type ComputerViewerNotification = { kind: "error" | "info"; message: string };
52
+
53
+ export type ComputerViewerProps = EmbeddedComputerInteractionClientOverride & {
54
+ /** Selected OpenGeni agent/session. Peer ComputerSessions remain visible. */
55
+ sessionId: string;
56
+ enabled?: boolean | undefined;
57
+ className?: string | undefined;
58
+ onNotify?: ((notification: ComputerViewerNotification) => void) | undefined;
59
+ /** Tests/demos only. Production uses the browser's native WebSocket. */
60
+ webSocketFactory?: ComputerFrameWebSocketFactory | undefined;
61
+ renderEmpty?: ((create: () => void, creating: boolean) => ReactNode) | undefined;
62
+ /** Host navigation request for one exact ComputerSession. */
63
+ requestedComputerSessionId?: string | null | undefined;
64
+ requestedComputerRequestId?: string | number | null | undefined;
65
+ };
66
+
67
+ type ComputerSelection = { sessionId: string; pinned: boolean } | null;
68
+ type PointerStart = { x: number; y: number; pointerId: number; frame: ComputerFrame };
69
+
70
+ /** Full ComputerSession surface: workspace discovery, peer switching, app/window/
71
+ * screen navigation, semantic controls, exact-frame input, and live media. */
72
+ export function ComputerViewer({
73
+ sessionId,
74
+ enabled = true,
75
+ className,
76
+ onNotify,
77
+ webSocketFactory,
78
+ renderEmpty,
79
+ requestedComputerSessionId,
80
+ requestedComputerRequestId,
81
+ ...override
82
+ }: ComputerViewerProps) {
83
+ const registry = useComputerSessions({ ...override, sessionId, enabled });
84
+ const createSession = registry.create;
85
+ const refreshRegistry = registry.refresh;
86
+ const liveRelevant = useMemo(
87
+ () => registry.relevantSessions.filter((session) => isLiveComputer(session)),
88
+ [registry.relevantSessions],
89
+ );
90
+ const recentRelevantFailure = useMemo(
91
+ () =>
92
+ liveRelevant.length === 0
93
+ ? (registry.relevantSessions.find((session) =>
94
+ ["failed", "lost"].includes(session.lifecycle),
95
+ ) ?? null)
96
+ : null,
97
+ [liveRelevant.length, registry.relevantSessions],
98
+ );
99
+ const liveSessions = useMemo(() => {
100
+ const sessions = registry.sessions.filter((session) => isLiveComputer(session));
101
+ return recentRelevantFailure ? [...sessions, recentRelevantFailure] : sessions;
102
+ }, [recentRelevantFailure, registry.sessions]);
103
+ const relevant = useMemo(
104
+ () =>
105
+ liveRelevant.length > 0 ? liveRelevant : recentRelevantFailure ? [recentRelevantFailure] : [],
106
+ [liveRelevant, recentRelevantFailure],
107
+ );
108
+ const [selection, setSelection] = useState<ComputerSelection>(null);
109
+ const interventions = useInteractionInterventions({
110
+ ...override,
111
+ enabled,
112
+ resourceKind: "computer_session",
113
+ });
114
+ const [creating, setCreating] = useState(false);
115
+ const [showControls, setShowControls] = useState(false);
116
+ const previousSessionIdRef = useRef(sessionId);
117
+ const handledRequestRef = useRef<string | null>(null);
118
+ const seenInterventionIdsRef = useRef(new Set<string>());
119
+
120
+ const notifyError = useCallback(
121
+ (cause: unknown, fallback: string) => {
122
+ onNotify?.({
123
+ kind: "error",
124
+ message: cause instanceof Error ? cause.message : fallback,
125
+ });
126
+ },
127
+ [onNotify],
128
+ );
129
+
130
+ useEffect(() => {
131
+ if (previousSessionIdRef.current === sessionId) return;
132
+ previousSessionIdRef.current = sessionId;
133
+ handledRequestRef.current = null;
134
+ setSelection(null);
135
+ }, [sessionId]);
136
+
137
+ useEffect(() => {
138
+ const requested = requestedComputerSessionId
139
+ ? (liveSessions.find((session) => session.id === requestedComputerSessionId) ?? null)
140
+ : null;
141
+ const requestKey = requested
142
+ ? `${requestedComputerRequestId ?? "default"}:${requested.id}`
143
+ : null;
144
+ if (requested && requestKey && handledRequestRef.current !== requestKey) {
145
+ handledRequestRef.current = requestKey;
146
+ setSelection({ sessionId: requested.id, pinned: true });
147
+ return;
148
+ }
149
+
150
+ const selectedStillLive = liveSessions.some((session) => session.id === selection?.sessionId);
151
+ // Peer computers remain one click away, but never become the implicit
152
+ // computer for another agent. Otherwise opening a sandbox's Computer tab
153
+ // can silently select—and send input to—a connected user's Mac.
154
+ if (selection?.pinned && selectedStillLive) return;
155
+ const preferred = relevant[0] ?? null;
156
+ if (!preferred) {
157
+ if (selection) setSelection(null);
158
+ return;
159
+ }
160
+ if (!selectedStillLive || !selection?.pinned) {
161
+ if (selection?.sessionId !== preferred.id || selection.pinned) {
162
+ setSelection({ sessionId: preferred.id, pinned: false });
163
+ }
164
+ }
165
+ }, [liveSessions, relevant, requestedComputerRequestId, requestedComputerSessionId, selection]);
166
+
167
+ const selectedRegistrySession = useMemo(
168
+ () => liveSessions.find((session) => session.id === selection?.sessionId) ?? null,
169
+ [liveSessions, selection?.sessionId],
170
+ );
171
+ const interventionCounts = useMemo(
172
+ () => countInterventions(interventions.interventions),
173
+ [interventions.interventions],
174
+ );
175
+ const selectedInterventions = useMemo(
176
+ () =>
177
+ interventions.interventions.filter(
178
+ (intervention) => intervention.resourceId === selection?.sessionId,
179
+ ),
180
+ [interventions.interventions, selection?.sessionId],
181
+ );
182
+ const controllerReady = selectedRegistrySession?.lifecycle === "active";
183
+ const computer = useComputerSession({
184
+ ...override,
185
+ computerSessionId: selection?.sessionId ?? null,
186
+ enabled: enabled && selection !== null && controllerReady,
187
+ });
188
+ const act = computer.act;
189
+ const actFromFrame = computer.actFromFrame;
190
+ const refreshComputer = computer.refresh;
191
+ const frames = useComputerFrameStream({
192
+ ...override,
193
+ computerSessionId: selection?.sessionId ?? null,
194
+ targetId: computer.selectedTarget?.id ?? null,
195
+ enabled: enabled && selection !== null && controllerReady && computer.selectedTarget !== null,
196
+ stream: {
197
+ format: "jpeg",
198
+ quality: 78,
199
+ maxWidth: 1_920,
200
+ maxHeight: 1_200,
201
+ everyNthFrame: 1,
202
+ },
203
+ ...(webSocketFactory ? { webSocketFactory } : {}),
204
+ });
205
+ const displayedFrame =
206
+ frames.frame &&
207
+ frames.frame.computerSessionId === selection?.sessionId &&
208
+ frames.frame.targetId === computer.selectedTarget?.id
209
+ ? frames.frame
210
+ : null;
211
+ const rfbStream =
212
+ frames.attachment?.stream.kind === "direct_rfb" ? frames.attachment.stream : null;
213
+ const rfbCapability = useMemo<DesktopStreamCapability | null>(() => {
214
+ if (!rfbStream || !frames.attachment) return null;
215
+ const bounds = computer.selectedTarget?.bounds;
216
+ return {
217
+ transport: "vnc-ws",
218
+ client: "novnc",
219
+ mode: "interactive",
220
+ url: rfbStream.url,
221
+ token: null,
222
+ expiresAt: frames.attachment.expiresAt,
223
+ resolution: [bounds?.width ?? 1_440, bounds?.height ?? 900],
224
+ unredacted: true,
225
+ requiresAcknowledgment: false,
226
+ acknowledged: true,
227
+ shared: false,
228
+ sharedSessionIds: [],
229
+ reason: null,
230
+ };
231
+ }, [computer.selectedTarget?.bounds, frames.attachment, rfbStream]);
232
+ const machineLocked = selectedRegistrySession?.failureCode === "machine_locked";
233
+ const currentIds = useMemo(() => new Set(relevant.map((session) => session.id)), [relevant]);
234
+
235
+ useEffect(() => {
236
+ for (const intervention of interventions.interventions) {
237
+ if (seenInterventionIdsRef.current.has(intervention.id)) continue;
238
+ seenInterventionIdsRef.current.add(intervention.id);
239
+ onNotify?.({
240
+ kind: "info",
241
+ message: `${interventionTitle(intervention)}: ${intervention.reason}`,
242
+ });
243
+ }
244
+ }, [interventions.interventions, onNotify]);
245
+
246
+ const resolveIntervention = useCallback(
247
+ (intervention: InteractionIntervention, outcome: "completed" | "dismissed") => {
248
+ void interventions
249
+ .resolve(intervention.id, {
250
+ expectedVersion: intervention.version,
251
+ outcome,
252
+ })
253
+ .then(() => {
254
+ onNotify?.({
255
+ kind: "info",
256
+ message:
257
+ outcome === "completed" ? "Agent notified. Continuing work." : "Request cancelled.",
258
+ });
259
+ })
260
+ .catch((cause) => notifyError(cause, "Could not update the computer request."));
261
+ },
262
+ [interventions, notifyError, onNotify],
263
+ );
264
+
265
+ const createComputer = useCallback(() => {
266
+ if (creating) return;
267
+ setCreating(true);
268
+ void createSession({ sessionId, name: "Computer" })
269
+ .then((response) => {
270
+ setSelection({ sessionId: response.session.id, pinned: false });
271
+ })
272
+ .catch((cause) => notifyError(cause, "Could not open a computer."))
273
+ .finally(() => setCreating(false));
274
+ }, [createSession, creating, notifyError, sessionId]);
275
+
276
+ const perform = useCallback(
277
+ async (action: ComputerAction, frame: ComputerFrame | null): Promise<void> => {
278
+ if (action.type === "pointer") {
279
+ if (!frame) throw new Error("Computer view is not ready for pointer input.");
280
+ await actFromFrame(action, frame);
281
+ } else {
282
+ await act(action);
283
+ }
284
+ },
285
+ [act, actFromFrame],
286
+ );
287
+
288
+ const copyFromRfb = useCallback(
289
+ (event: ClipboardEvent<HTMLDivElement>) => {
290
+ if (!rfbStream || computer.session?.capabilities?.clipboard !== true) return;
291
+ event.preventDefault();
292
+ event.stopPropagation();
293
+ void perform({ type: "clipboard", operation: "copy" }, null)
294
+ .then(() => computer.readClipboard())
295
+ .then(async (clipboard) => {
296
+ if (clipboard.text && !(await copyTextToClipboard(clipboard.text))) {
297
+ throw new Error("Computer text could not be copied to the local clipboard");
298
+ }
299
+ })
300
+ .catch((cause) => notifyError(cause, "Could not copy from the computer."));
301
+ },
302
+ [computer, notifyError, perform, rfbStream],
303
+ );
304
+
305
+ const pasteIntoRfb = useCallback(
306
+ (text: string): boolean => {
307
+ if (!rfbStream || computer.session?.capabilities?.clipboard !== true) return false;
308
+ // Keep paste on the canonical ComputerSession action path. RFB
309
+ // ClientCutText synchronization varies by server and can acknowledge a
310
+ // local paste without ever updating the remote graphical seat. These two
311
+ // awaited actions run against the exact selected ComputerSession/target.
312
+ // Read-after-write is the causal readiness barrier on X11: clipboard
313
+ // ownership is asynchronous, so a successful write response alone does
314
+ // not prove that the graphical seat can already serve the selection.
315
+ void (async () => {
316
+ await perform({ type: "clipboard", operation: "write", text }, null);
317
+ assertExactComputerClipboard(await computer.readClipboard(), text);
318
+ await perform({ type: "clipboard", operation: "paste" }, null);
319
+ })().catch((cause) => notifyError(cause, "Could not paste into the computer."));
320
+ return true;
321
+ },
322
+ [computer, notifyError, perform, rfbStream],
323
+ );
324
+
325
+ if (!enabled) return null;
326
+ if (registry.loading && liveSessions.length === 0) {
327
+ return (
328
+ <ComputerNotice
329
+ icon={<LoaderCircleIcon className="size-4 animate-spin" />}
330
+ text="Finding workspace computers…"
331
+ {...(className ? { className } : {})}
332
+ />
333
+ );
334
+ }
335
+ if (liveSessions.length === 0) {
336
+ if (renderEmpty) return renderEmpty(createComputer, creating);
337
+ return (
338
+ <div className={cn("grid h-full place-items-center bg-og-bg p-6", className)}>
339
+ <div className="max-w-sm text-center">
340
+ <span className="mx-auto grid size-10 place-items-center rounded-og-lg border border-og-border bg-og-surface-1 text-og-muted">
341
+ <MonitorIcon className="size-5" />
342
+ </span>
343
+ <p className="mt-3 text-og-menu font-medium text-og-fg">No computer open</p>
344
+ <p className="mt-1 text-og-control leading-5 text-og-muted">
345
+ Open this agent&apos;s desktop, or switch to any computer another workspace agent uses.
346
+ </p>
347
+ <button
348
+ type="button"
349
+ onClick={createComputer}
350
+ disabled={creating}
351
+ className="mt-4 inline-flex h-8 items-center gap-1.5 rounded-og-sm bg-og-accent-deep px-3 text-og-control font-medium text-og-accent-fg transition hover:brightness-110 disabled:opacity-50"
352
+ >
353
+ {creating ? (
354
+ <LoaderCircleIcon className="size-3.5 animate-spin" />
355
+ ) : (
356
+ <PlusIcon className="size-3.5" />
357
+ )}
358
+ Open computer
359
+ </button>
360
+ {registry.error ? (
361
+ <p className="mt-3 text-og-control text-og-status-error">{registry.error.message}</p>
362
+ ) : null}
363
+ </div>
364
+ </div>
365
+ );
366
+ }
367
+
368
+ return (
369
+ <div className={cn("flex h-full min-h-0 flex-col overflow-hidden bg-og-bg", className)}>
370
+ <ComputerToolbar
371
+ sessions={liveSessions}
372
+ relevantSessionIds={currentIds}
373
+ selectedSessionId={selection?.sessionId ?? null}
374
+ creating={creating}
375
+ refreshing={registry.refreshing}
376
+ interventionCounts={interventionCounts}
377
+ onSelect={(computerSessionId) =>
378
+ setSelection({ sessionId: computerSessionId, pinned: true })
379
+ }
380
+ onFollow={() => {
381
+ if (relevant[0]) setSelection({ sessionId: relevant[0].id, pinned: false });
382
+ }}
383
+ onCreate={createComputer}
384
+ onRefresh={() => void Promise.all([refreshRegistry(), refreshComputer()])}
385
+ />
386
+ <InteractionInterventionBanner
387
+ interventions={selectedInterventions}
388
+ activeTargetId={computer.selectedTarget?.id ?? null}
389
+ mutating={interventions.mutating}
390
+ onOpen={(intervention) =>
391
+ void computer
392
+ .selectTarget(intervention.targetId)
393
+ .catch((cause) => notifyError(cause, "Could not open the requested computer view."))
394
+ }
395
+ onResolve={resolveIntervention}
396
+ />
397
+ {!selectedRegistrySession ? (
398
+ <ComputerUnselectedPanel
399
+ peerCount={liveSessions.length}
400
+ creating={creating}
401
+ onCreate={createComputer}
402
+ />
403
+ ) : !controllerReady ? (
404
+ <ComputerLifecyclePanel
405
+ session={selectedRegistrySession}
406
+ creating={creating}
407
+ onRefresh={registry.refresh}
408
+ onRetry={createComputer}
409
+ />
410
+ ) : (
411
+ <>
412
+ <ComputerTargetRail
413
+ targets={computer.targets}
414
+ selectedTargetId={computer.selectedTarget?.id ?? null}
415
+ loading={computer.loading}
416
+ onSelect={(targetId) =>
417
+ void computer
418
+ .selectTarget(targetId)
419
+ .catch((cause) => notifyError(cause, "Could not switch computer views."))
420
+ }
421
+ />
422
+ <div className="flex min-h-0 flex-1">
423
+ {rfbStream ? (
424
+ <div className="relative min-h-0 flex-1 bg-black" onCopyCapture={copyFromRfb}>
425
+ <DesktopViewer
426
+ capability={rfbCapability}
427
+ interactive
428
+ showControlToggle={false}
429
+ webSocketProtocols={rfbStream.protocols}
430
+ onPasteText={pasteIntoRfb}
431
+ targetPlatform={computer.session?.platform ?? null}
432
+ className="h-full"
433
+ />
434
+ </div>
435
+ ) : (
436
+ <ComputerViewport
437
+ frame={machineLocked ? null : displayedFrame}
438
+ observation={computer.observation}
439
+ target={computer.selectedTarget}
440
+ machineLocked={machineLocked}
441
+ connectionState={frames.state}
442
+ connectionError={frames.error ?? computer.error}
443
+ mutating={computer.mutating}
444
+ backgroundActions={computer.session?.capabilities?.backgroundActions === true}
445
+ clipboardEnabled={computer.session?.capabilities?.clipboard === true}
446
+ onAction={perform}
447
+ onReadClipboard={computer.readClipboard}
448
+ onReconnect={frames.reconnect}
449
+ onError={(cause) => notifyError(cause, "Computer input failed.")}
450
+ />
451
+ )}
452
+ {showControls ? (
453
+ <ComputerSemanticPanel
454
+ observation={computer.observation}
455
+ mutating={computer.mutating}
456
+ onAction={(action) =>
457
+ void perform(action, null).catch((cause) =>
458
+ notifyError(cause, "Computer action failed."),
459
+ )
460
+ }
461
+ />
462
+ ) : null}
463
+ </div>
464
+ <ComputerStatusBar
465
+ session={computer.session}
466
+ target={computer.selectedTarget}
467
+ connectionState={frames.state}
468
+ refreshing={registry.refreshing}
469
+ showControls={showControls}
470
+ controlCount={semanticNodes(computer.observation).length}
471
+ onToggleControls={() => setShowControls((current) => !current)}
472
+ />
473
+ </>
474
+ )}
475
+ </div>
476
+ );
477
+ }
478
+
479
+ function ComputerUnselectedPanel(props: {
480
+ peerCount: number;
481
+ creating: boolean;
482
+ onCreate: () => void;
483
+ }) {
484
+ return (
485
+ <div className="grid min-h-0 flex-1 place-items-center bg-og-bg p-6 text-center">
486
+ <div className="max-w-sm">
487
+ <span className="mx-auto grid size-10 place-items-center rounded-og-md border border-og-border bg-og-surface-1 text-og-muted">
488
+ <MonitorIcon className="size-4.5" />
489
+ </span>
490
+ <p className="mt-3 text-og-menu font-medium text-og-fg">No computer for this agent</p>
491
+ <p className="mt-1 text-og-control leading-5 text-og-muted">
492
+ {props.peerCount === 1
493
+ ? "One workspace computer is available from the computer menu."
494
+ : `${props.peerCount} workspace computers are available from the computer menu.`}
495
+ </p>
496
+ <button
497
+ type="button"
498
+ disabled={props.creating}
499
+ onClick={props.onCreate}
500
+ className="mt-4 inline-flex h-8 items-center gap-1.5 rounded-og-sm border border-og-border bg-og-surface-1 px-3 text-og-control font-medium text-og-fg transition hover:bg-og-surface-2 disabled:cursor-not-allowed disabled:opacity-50"
501
+ >
502
+ {props.creating ? (
503
+ <LoaderCircleIcon className="size-3.5 animate-spin" />
504
+ ) : (
505
+ <PlusIcon className="size-3.5" />
506
+ )}
507
+ {props.creating ? "Opening…" : "New computer"}
508
+ </button>
509
+ </div>
510
+ </div>
511
+ );
512
+ }
513
+
514
+ function ComputerToolbar(props: {
515
+ sessions: ComputerSession[];
516
+ relevantSessionIds: Set<string>;
517
+ selectedSessionId: string | null;
518
+ creating: boolean;
519
+ refreshing: boolean;
520
+ interventionCounts: Map<string, number>;
521
+ onSelect: (id: string) => void;
522
+ onFollow: () => void;
523
+ onCreate: () => void;
524
+ onRefresh: () => void;
525
+ }) {
526
+ const detailsRef = useRef<HTMLDetailsElement | null>(null);
527
+ const selected = props.sessions.find((session) => session.id === props.selectedSessionId);
528
+ const current = props.sessions.filter((session) => props.relevantSessionIds.has(session.id));
529
+ const others = props.sessions.filter((session) => !props.relevantSessionIds.has(session.id));
530
+ const choose = (id: string) => {
531
+ props.onSelect(id);
532
+ detailsRef.current?.removeAttribute("open");
533
+ };
534
+ return (
535
+ <div className="flex h-10 shrink-0 items-center gap-1.5 border-b border-og-border bg-og-surface-0 px-2">
536
+ <details ref={detailsRef} className="relative min-w-0">
537
+ <summary className="flex h-7 max-w-52 cursor-pointer list-none items-center gap-2 rounded-og-sm px-2 text-og-control text-og-fg transition hover:bg-og-surface-2 [&::-webkit-details-marker]:hidden">
538
+ <MonitorIcon className="size-3.5 shrink-0 text-og-muted" />
539
+ <span className="truncate font-medium">{selected?.name ?? "Computer"}</span>
540
+ {(props.interventionCounts.get(selected?.id ?? "") ?? 0) > 0 ? (
541
+ <span className="size-1.5 shrink-0 rounded-full bg-og-status-waiting" />
542
+ ) : null}
543
+ <ChevronDownIcon className="size-3 shrink-0 text-og-subtle" />
544
+ </summary>
545
+ <div className="absolute left-0 top-8 z-30 w-72 overflow-hidden rounded-og-md border border-og-border bg-og-surface-1 p-1 shadow-xl">
546
+ <ComputerSessionGroup
547
+ label="Current agent"
548
+ sessions={current}
549
+ selectedId={props.selectedSessionId}
550
+ interventionCounts={props.interventionCounts}
551
+ onSelect={choose}
552
+ />
553
+ <ComputerSessionGroup
554
+ label="Other agents"
555
+ sessions={others}
556
+ selectedId={props.selectedSessionId}
557
+ interventionCounts={props.interventionCounts}
558
+ onSelect={choose}
559
+ />
560
+ <div className="mt-1 flex gap-1 border-t border-og-border pt-1">
561
+ {current.length > 0 ? (
562
+ <MenuButton onClick={props.onFollow}>Follow agent</MenuButton>
563
+ ) : null}
564
+ <MenuButton onClick={props.onCreate} disabled={props.creating}>
565
+ <PlusIcon className="size-3.5" /> New computer
566
+ </MenuButton>
567
+ </div>
568
+ </div>
569
+ </details>
570
+ <span className="min-w-0 flex-1 truncate text-og-xs text-og-subtle">
571
+ {selected ? `${platformLabel(selected)} · ${placementLabel(selected)}` : ""}
572
+ </span>
573
+ <button
574
+ type="button"
575
+ onClick={props.onRefresh}
576
+ className="grid size-7 place-items-center rounded-og-sm text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg"
577
+ aria-label="Refresh computers"
578
+ >
579
+ <RefreshCwIcon className={cn("size-3.5", props.refreshing && "animate-spin")} />
580
+ </button>
581
+ <button
582
+ type="button"
583
+ onClick={props.onCreate}
584
+ disabled={props.creating}
585
+ className="grid size-7 place-items-center rounded-og-sm text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg disabled:opacity-40"
586
+ aria-label="Open a new computer"
587
+ >
588
+ {props.creating ? (
589
+ <LoaderCircleIcon className="size-3.5 animate-spin" />
590
+ ) : (
591
+ <PlusIcon className="size-3.5" />
592
+ )}
593
+ </button>
594
+ </div>
595
+ );
596
+ }
597
+
598
+ function ComputerSessionGroup(props: {
599
+ label: string;
600
+ sessions: ComputerSession[];
601
+ selectedId: string | null;
602
+ interventionCounts: Map<string, number>;
603
+ onSelect: (id: string) => void;
604
+ }) {
605
+ if (props.sessions.length === 0) return null;
606
+ return (
607
+ <div className="py-1">
608
+ <p className="px-2 pb-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-subtle">
609
+ {props.label}
610
+ </p>
611
+ {props.sessions.map((session) => {
612
+ const interventionCount = props.interventionCounts.get(session.id) ?? 0;
613
+ return (
614
+ <button
615
+ key={session.id}
616
+ type="button"
617
+ onClick={() => props.onSelect(session.id)}
618
+ className={cn(
619
+ "flex w-full items-center gap-2 rounded-og-sm px-2 py-1.5 text-left transition hover:bg-og-surface-2",
620
+ session.id === props.selectedId && "bg-og-surface-2",
621
+ )}
622
+ >
623
+ <span
624
+ className={cn(
625
+ "size-1.5 rounded-full",
626
+ session.lifecycle === "active" ? "bg-og-status-running" : "bg-og-muted",
627
+ )}
628
+ />
629
+ <span className="min-w-0 flex-1">
630
+ <span className="block truncate text-og-control text-og-fg">{session.name}</span>
631
+ <span className="block truncate text-og-xs text-og-subtle">
632
+ {platformLabel(session)} · {placementLabel(session)}
633
+ </span>
634
+ </span>
635
+ {interventionCount > 0 ? (
636
+ <span className="rounded-full bg-og-status-waiting/10 px-1.5 py-0.5 text-[10px] font-medium text-og-status-waiting">
637
+ {interventionCount}
638
+ </span>
639
+ ) : null}
640
+ </button>
641
+ );
642
+ })}
643
+ </div>
644
+ );
645
+ }
646
+
647
+ function MenuButton(props: { children: ReactNode; onClick: () => void; disabled?: boolean }) {
648
+ return (
649
+ <button
650
+ type="button"
651
+ onClick={props.onClick}
652
+ disabled={props.disabled}
653
+ className="flex h-7 flex-1 items-center justify-center gap-1 rounded-og-sm px-2 text-og-control text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg disabled:opacity-50"
654
+ >
655
+ {props.children}
656
+ </button>
657
+ );
658
+ }
659
+
660
+ function ComputerTargetRail(props: {
661
+ targets: ComputerTarget[];
662
+ selectedTargetId: string | null;
663
+ loading: boolean;
664
+ onSelect: (id: string) => void;
665
+ }) {
666
+ const visualTargets = props.targets.filter(isRenderableComputerView);
667
+ return (
668
+ <div
669
+ className="flex h-10 shrink-0 items-center gap-1 overflow-x-auto border-b border-og-border bg-og-surface-0 px-2"
670
+ aria-label="Computer views"
671
+ >
672
+ {visualTargets.map((target) => (
673
+ <button
674
+ key={target.id}
675
+ type="button"
676
+ onClick={() => props.onSelect(target.id)}
677
+ className={cn(
678
+ "flex h-7 max-w-56 shrink-0 items-center gap-1.5 rounded-og-sm border px-2 text-og-control transition",
679
+ target.id === props.selectedTargetId
680
+ ? "border-og-border-strong bg-og-surface-2 text-og-fg"
681
+ : "border-transparent text-og-muted hover:bg-og-surface-1 hover:text-og-fg",
682
+ )}
683
+ aria-pressed={target.id === props.selectedTargetId}
684
+ >
685
+ {target.kind === "screen" ? (
686
+ <MonitorIcon className="size-3.5 shrink-0" />
687
+ ) : (
688
+ <PanelsTopLeftIcon className="size-3.5 shrink-0" />
689
+ )}
690
+ <span className="truncate">{target.title || target.applicationId || target.kind}</span>
691
+ {target.focused ? (
692
+ <span
693
+ className="size-1.5 shrink-0 rounded-full bg-og-status-running"
694
+ aria-label="Focused"
695
+ />
696
+ ) : null}
697
+ </button>
698
+ ))}
699
+ {props.loading ? (
700
+ <LoaderCircleIcon className="ml-1 size-3.5 animate-spin text-og-muted" />
701
+ ) : null}
702
+ {!props.loading && visualTargets.length === 0 ? (
703
+ <span className="text-og-xs text-og-subtle">Waiting for apps and screens…</span>
704
+ ) : null}
705
+ </div>
706
+ );
707
+ }
708
+
709
+ function isRenderableComputerView(target: ComputerTarget): boolean {
710
+ if (target.kind === "screen") return true;
711
+ if (target.kind !== "window" || !target.bounds) return false;
712
+ // AX exposes applications and tiny utility/menu windows as valid semantic
713
+ // targets. Keep those available to tools, but do not present them as visual
714
+ // desktop tabs when they cannot form a useful live view.
715
+ return target.bounds.width >= 160 && target.bounds.height >= 90;
716
+ }
717
+
718
+ function ComputerLifecyclePanel(props: {
719
+ session: ComputerSession;
720
+ creating: boolean;
721
+ onRefresh: () => Promise<void>;
722
+ onRetry: () => void;
723
+ }) {
724
+ const failed = ["failed", "lost", "repair_required"].includes(props.session.lifecycle);
725
+ const retryCreatesSession = ["failed", "lost"].includes(props.session.lifecycle);
726
+ return (
727
+ <div className="grid min-h-0 flex-1 place-items-center bg-og-bg p-6">
728
+ <div className="max-w-sm text-center">
729
+ {failed ? (
730
+ <CircleAlertIcon className="mx-auto size-5 text-og-status-error" />
731
+ ) : (
732
+ <LoaderCircleIcon className="mx-auto size-5 animate-spin text-og-muted" />
733
+ )}
734
+ <p className="mt-3 text-og-menu font-medium text-og-fg">
735
+ {failed ? "Computer needs attention" : lifecycleLabel(props.session.lifecycle)}
736
+ </p>
737
+ <p className="mt-1 text-og-control leading-5 text-og-muted">
738
+ {computerFailureMessage(props.session) ??
739
+ "The computer is being prepared on its placement. It will appear here when ready."}
740
+ </p>
741
+ {failed ? (
742
+ <button
743
+ type="button"
744
+ onClick={() => (retryCreatesSession ? props.onRetry() : void props.onRefresh())}
745
+ disabled={props.creating}
746
+ 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"
747
+ >
748
+ {props.creating ? (
749
+ <LoaderCircleIcon className="size-3.5 animate-spin" />
750
+ ) : (
751
+ <RotateCcwIcon className="size-3.5" />
752
+ )}
753
+ {retryCreatesSession ? "Try again" : "Check again"}
754
+ </button>
755
+ ) : null}
756
+ </div>
757
+ </div>
758
+ );
759
+ }
760
+
761
+ function ComputerViewport(props: {
762
+ frame: ComputerFrame | null;
763
+ observation: ComputerObservation | null;
764
+ target: ComputerTarget | null;
765
+ machineLocked: boolean;
766
+ connectionState: string;
767
+ connectionError: Error | null;
768
+ mutating: boolean;
769
+ backgroundActions: boolean;
770
+ clipboardEnabled: boolean;
771
+ onAction: (action: ComputerAction, frame: ComputerFrame | null) => Promise<void>;
772
+ onReadClipboard: () => Promise<ComputerClipboard>;
773
+ onReconnect: () => void;
774
+ onError: (cause: unknown) => void;
775
+ }) {
776
+ const canvasRef = useRef<HTMLCanvasElement | null>(null);
777
+ const inputRef = useRef<HTMLTextAreaElement | null>(null);
778
+ const pointerStartRef = useRef<PointerStart | null>(null);
779
+ const clickTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
780
+ const lastClickRef = useRef<{
781
+ at: number;
782
+ x: number;
783
+ y: number;
784
+ frame: ComputerFrame;
785
+ } | null>(null);
786
+ const wheelRef = useRef<{
787
+ x: number;
788
+ y: number;
789
+ deltaX: number;
790
+ deltaY: number;
791
+ frame: ComputerFrame;
792
+ timer: ReturnType<typeof setTimeout> | null;
793
+ } | null>(null);
794
+ const pendingTextRef = useRef<{
795
+ text: string;
796
+ timer: ReturnType<typeof setTimeout>;
797
+ } | null>(null);
798
+ const actionRef = useRef(props.onAction);
799
+ const readClipboardRef = useRef(props.onReadClipboard);
800
+ const errorRef = useRef(props.onError);
801
+ const actionTailRef = useRef<Promise<void>>(Promise.resolve());
802
+ const queuedFrameRef = useRef<ComputerFrame | null>(null);
803
+ const decodingFrameRef = useRef(false);
804
+ const mountedRef = useRef(true);
805
+ actionRef.current = props.onAction;
806
+ readClipboardRef.current = props.onReadClipboard;
807
+ errorRef.current = props.onError;
808
+ const rawInputEnabled =
809
+ !props.backgroundActions || props.target?.kind === "screen" || props.target?.focused === true;
810
+
811
+ const paintQueuedFrames = useCallback(() => {
812
+ if (decodingFrameRef.current) return;
813
+ decodingFrameRef.current = true;
814
+ void (async () => {
815
+ try {
816
+ while (mountedRef.current) {
817
+ const frame = queuedFrameRef.current;
818
+ queuedFrameRef.current = null;
819
+ if (!frame) break;
820
+ let objectUrl: string | null = null;
821
+ try {
822
+ const blob = new Blob([frame.data.slice().buffer], { type: frame.mediaType });
823
+ if (typeof createImageBitmap === "function") {
824
+ const bitmap = await createImageBitmap(blob);
825
+ try {
826
+ const canvas = canvasRef.current;
827
+ if (mountedRef.current && canvas) {
828
+ paintCanvas(canvas, bitmap, frame.width, frame.height);
829
+ }
830
+ } finally {
831
+ bitmap.close();
832
+ }
833
+ continue;
834
+ }
835
+ objectUrl = URL.createObjectURL(blob);
836
+ const image = await loadImage(objectUrl);
837
+ const canvas = canvasRef.current;
838
+ if (mountedRef.current && canvas) {
839
+ paintCanvas(canvas, image, frame.width, frame.height);
840
+ }
841
+ } catch (cause) {
842
+ if (mountedRef.current) errorRef.current(cause);
843
+ } finally {
844
+ if (objectUrl) URL.revokeObjectURL(objectUrl);
845
+ }
846
+ }
847
+ } finally {
848
+ decodingFrameRef.current = false;
849
+ if (mountedRef.current && queuedFrameRef.current) paintQueuedFrames();
850
+ }
851
+ })();
852
+ }, []);
853
+
854
+ useEffect(() => {
855
+ mountedRef.current = true;
856
+ return () => {
857
+ mountedRef.current = false;
858
+ queuedFrameRef.current = null;
859
+ };
860
+ }, []);
861
+
862
+ useEffect(() => {
863
+ if (!props.frame) return;
864
+ queuedFrameRef.current = props.frame;
865
+ paintQueuedFrames();
866
+ }, [paintQueuedFrames, props.frame]);
867
+
868
+ useEffect(
869
+ () => () => {
870
+ if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
871
+ if (wheelRef.current?.timer) clearTimeout(wheelRef.current.timer);
872
+ if (pendingTextRef.current?.timer) clearTimeout(pendingTextRef.current.timer);
873
+ },
874
+ [],
875
+ );
876
+
877
+ const enqueue = useCallback(
878
+ (action: ComputerAction, frame: ComputerFrame | null, after?: () => Promise<void>) => {
879
+ actionTailRef.current = actionTailRef.current
880
+ .catch(() => undefined)
881
+ .then(async () => {
882
+ await actionRef.current(action, frame);
883
+ await after?.();
884
+ })
885
+ .catch((cause) => errorRef.current(cause));
886
+ },
887
+ [],
888
+ );
889
+
890
+ const point = useCallback(
891
+ (frame: ComputerFrame, clientX: number, clientY: number) =>
892
+ computerPoint(canvasRef.current, frame, clientX, clientY),
893
+ [],
894
+ );
895
+
896
+ const flushPendingText = useCallback(() => {
897
+ const pending = pendingTextRef.current;
898
+ if (!pending) return;
899
+ clearTimeout(pending.timer);
900
+ pendingTextRef.current = null;
901
+ enqueue({ type: "keyboard", action: "type", value: pending.text }, null);
902
+ }, [enqueue]);
903
+
904
+ const flushPendingClick = useCallback(() => {
905
+ const pending = lastClickRef.current;
906
+ if (!pending) return;
907
+ if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
908
+ clickTimerRef.current = null;
909
+ lastClickRef.current = null;
910
+ enqueue(
911
+ {
912
+ type: "pointer",
913
+ frameId: pending.frame.frameId,
914
+ action: "click",
915
+ x: pending.x,
916
+ y: pending.y,
917
+ },
918
+ pending.frame,
919
+ );
920
+ }, [enqueue]);
921
+
922
+ const pointerDown = (event: PointerEvent<HTMLCanvasElement>) => {
923
+ if (!props.frame || props.mutating || !rawInputEnabled || event.button !== 0) return;
924
+ flushPendingText();
925
+ pointerStartRef.current = {
926
+ x: event.clientX,
927
+ y: event.clientY,
928
+ pointerId: event.pointerId,
929
+ frame: props.frame,
930
+ };
931
+ event.currentTarget.setPointerCapture?.(event.pointerId);
932
+ inputRef.current?.focus({ preventScroll: true });
933
+ };
934
+
935
+ const pointerUp = (event: PointerEvent<HTMLCanvasElement>) => {
936
+ if (!rawInputEnabled) return;
937
+ const start = pointerStartRef.current;
938
+ pointerStartRef.current = null;
939
+ if (!start || start.pointerId !== event.pointerId) return;
940
+ const from = point(start.frame, start.x, start.y);
941
+ const to = point(start.frame, event.clientX, event.clientY);
942
+ if (!from || !to) return;
943
+ if (Math.hypot(event.clientX - start.x, event.clientY - start.y) > 5) {
944
+ flushPendingClick();
945
+ enqueue(
946
+ {
947
+ type: "pointer",
948
+ frameId: start.frame.frameId,
949
+ action: "drag",
950
+ x: from.x,
951
+ y: from.y,
952
+ endX: to.x,
953
+ endY: to.y,
954
+ },
955
+ start.frame,
956
+ );
957
+ return;
958
+ }
959
+ const now = Date.now();
960
+ const previous = lastClickRef.current;
961
+ if (
962
+ previous &&
963
+ now - previous.at < 280 &&
964
+ Math.hypot(previous.x - to.x, previous.y - to.y) < 6 &&
965
+ sameFrameFence(previous.frame, start.frame)
966
+ ) {
967
+ if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
968
+ clickTimerRef.current = null;
969
+ lastClickRef.current = null;
970
+ enqueue(
971
+ {
972
+ type: "pointer",
973
+ frameId: start.frame.frameId,
974
+ action: "double_click",
975
+ x: to.x,
976
+ y: to.y,
977
+ },
978
+ start.frame,
979
+ );
980
+ return;
981
+ }
982
+ lastClickRef.current = { at: now, x: to.x, y: to.y, frame: start.frame };
983
+ clickTimerRef.current = setTimeout(() => {
984
+ clickTimerRef.current = null;
985
+ lastClickRef.current = null;
986
+ enqueue(
987
+ {
988
+ type: "pointer",
989
+ frameId: start.frame.frameId,
990
+ action: "click",
991
+ x: to.x,
992
+ y: to.y,
993
+ },
994
+ start.frame,
995
+ );
996
+ }, 280);
997
+ };
998
+
999
+ const contextMenu = (event: MouseEvent<HTMLCanvasElement>) => {
1000
+ event.preventDefault();
1001
+ if (!rawInputEnabled) return;
1002
+ const frame = props.frame;
1003
+ if (!frame) return;
1004
+ flushPendingText();
1005
+ flushPendingClick();
1006
+ const at = point(frame, event.clientX, event.clientY);
1007
+ if (at) {
1008
+ enqueue(
1009
+ {
1010
+ type: "pointer",
1011
+ frameId: frame.frameId,
1012
+ action: "click",
1013
+ x: at.x,
1014
+ y: at.y,
1015
+ button: "right",
1016
+ },
1017
+ frame,
1018
+ );
1019
+ }
1020
+ };
1021
+
1022
+ const wheel = (event: WheelEvent<HTMLCanvasElement>) => {
1023
+ if (!rawInputEnabled) return;
1024
+ const frame = props.frame;
1025
+ if (!frame) return;
1026
+ const at = point(frame, event.clientX, event.clientY);
1027
+ if (!at) return;
1028
+ flushPendingText();
1029
+ flushPendingClick();
1030
+ event.preventDefault();
1031
+ const pending = wheelRef.current;
1032
+ if (pending?.timer) clearTimeout(pending.timer);
1033
+ wheelRef.current = {
1034
+ x: at.x,
1035
+ y: at.y,
1036
+ deltaX: (pending && sameFrameFence(pending.frame, frame) ? pending.deltaX : 0) + event.deltaX,
1037
+ deltaY: (pending && sameFrameFence(pending.frame, frame) ? pending.deltaY : 0) + event.deltaY,
1038
+ frame,
1039
+ timer: setTimeout(() => {
1040
+ const batch = wheelRef.current;
1041
+ wheelRef.current = null;
1042
+ if (batch) {
1043
+ enqueue(
1044
+ {
1045
+ type: "pointer",
1046
+ frameId: batch.frame.frameId,
1047
+ action: "scroll",
1048
+ x: batch.x,
1049
+ y: batch.y,
1050
+ deltaX: batch.deltaX,
1051
+ deltaY: batch.deltaY,
1052
+ },
1053
+ batch.frame,
1054
+ );
1055
+ }
1056
+ }, 45),
1057
+ };
1058
+ };
1059
+
1060
+ const keyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
1061
+ if (!rawInputEnabled) return;
1062
+ const command = event.metaKey || event.ctrlKey;
1063
+ if (
1064
+ props.clipboardEnabled &&
1065
+ command &&
1066
+ !event.altKey &&
1067
+ ["c", "v"].includes(event.key.toLowerCase())
1068
+ ) {
1069
+ flushPendingText();
1070
+ return;
1071
+ }
1072
+ const key = computerKey(event);
1073
+ if (!key) return;
1074
+ event.preventDefault();
1075
+ flushPendingText();
1076
+ flushPendingClick();
1077
+ enqueue({ type: "keyboard", action: "press", value: key }, null);
1078
+ };
1079
+
1080
+ const copy = (event: ClipboardEvent<HTMLTextAreaElement>) => {
1081
+ if (!props.clipboardEnabled || !rawInputEnabled) return;
1082
+ event.preventDefault();
1083
+ flushPendingText();
1084
+ flushPendingClick();
1085
+ enqueue({ type: "clipboard", operation: "copy" }, null, async () => {
1086
+ const clipboard = await readClipboardRef.current();
1087
+ if (!clipboard.text) return;
1088
+ if (!(await copyTextToClipboard(clipboard.text))) {
1089
+ throw new Error("Computer text could not be copied to the local clipboard");
1090
+ }
1091
+ });
1092
+ };
1093
+
1094
+ const paste = (event: ClipboardEvent<HTMLTextAreaElement>) => {
1095
+ if (!props.clipboardEnabled || !rawInputEnabled) return;
1096
+ event.preventDefault();
1097
+ flushPendingText();
1098
+ flushPendingClick();
1099
+ const text = event.clipboardData.getData("text/plain");
1100
+ enqueue({ type: "clipboard", operation: "write", text }, null, async () => {
1101
+ assertExactComputerClipboard(await readClipboardRef.current(), text);
1102
+ await actionRef.current({ type: "clipboard", operation: "paste" }, null);
1103
+ });
1104
+ };
1105
+
1106
+ const input = (value: string) => {
1107
+ if (!value || !rawInputEnabled) return;
1108
+ flushPendingClick();
1109
+ const pending = pendingTextRef.current;
1110
+ if (pending) {
1111
+ clearTimeout(pending.timer);
1112
+ pending.text += value;
1113
+ pending.timer = setTimeout(flushPendingText, 16);
1114
+ } else {
1115
+ pendingTextRef.current = {
1116
+ text: value,
1117
+ timer: setTimeout(flushPendingText, 16),
1118
+ };
1119
+ }
1120
+ if (inputRef.current) inputRef.current.value = "";
1121
+ };
1122
+
1123
+ const showCanvas = props.frame !== null && !props.machineLocked;
1124
+ return (
1125
+ <div className="relative min-h-0 flex-1 overflow-hidden bg-black">
1126
+ <canvas
1127
+ ref={canvasRef}
1128
+ className={cn(
1129
+ "absolute inset-0 m-auto max-h-full max-w-full touch-none focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent",
1130
+ !showCanvas && "invisible",
1131
+ !rawInputEnabled && "cursor-default",
1132
+ )}
1133
+ onPointerDown={pointerDown}
1134
+ onPointerUp={pointerUp}
1135
+ onPointerCancel={() => {
1136
+ pointerStartRef.current = null;
1137
+ }}
1138
+ onContextMenu={contextMenu}
1139
+ onWheel={wheel}
1140
+ aria-label={`Interactive ${props.target?.kind ?? "computer"} view`}
1141
+ />
1142
+ <textarea
1143
+ ref={inputRef}
1144
+ defaultValue=""
1145
+ onInput={(event) => input(event.currentTarget.value)}
1146
+ onKeyDown={keyDown}
1147
+ onCopy={copy}
1148
+ onPaste={paste}
1149
+ className="pointer-events-none absolute left-1/2 top-1/2 size-px resize-none overflow-hidden opacity-0"
1150
+ aria-label="Computer keyboard input"
1151
+ autoCapitalize="off"
1152
+ autoCorrect="off"
1153
+ spellCheck={false}
1154
+ disabled={!rawInputEnabled}
1155
+ />
1156
+ {!showCanvas ? (
1157
+ <ComputerViewportFallback
1158
+ observation={props.observation}
1159
+ machineLocked={props.machineLocked}
1160
+ connectionState={props.connectionState}
1161
+ error={props.connectionError}
1162
+ onAction={(action) => enqueue(action, null)}
1163
+ onReconnect={props.onReconnect}
1164
+ />
1165
+ ) : null}
1166
+ {props.mutating ? (
1167
+ <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">
1168
+ <LoaderCircleIcon className="size-3 animate-spin" /> Acting
1169
+ </div>
1170
+ ) : null}
1171
+ {showCanvas && !rawInputEnabled ? (
1172
+ <div className="absolute bottom-3 left-1/2 flex -translate-x-1/2 items-center gap-2 rounded-full border border-white/10 bg-black/70 py-1 pl-3 pr-1 text-[11px] text-white/80 backdrop-blur">
1173
+ <span>Background view · use native controls</span>
1174
+ {props.target ? (
1175
+ <button
1176
+ type="button"
1177
+ disabled={props.mutating}
1178
+ onClick={() => enqueue({ type: "focus", targetId: props.target!.id }, null)}
1179
+ className="rounded-full bg-white/10 px-2 py-0.5 font-medium text-white transition hover:bg-white/20 disabled:opacity-50"
1180
+ >
1181
+ Control directly
1182
+ </button>
1183
+ ) : null}
1184
+ </div>
1185
+ ) : null}
1186
+ </div>
1187
+ );
1188
+ }
1189
+
1190
+ function assertExactComputerClipboard(clipboard: ComputerClipboard, expected: string): void {
1191
+ if (clipboard.truncated || clipboard.text !== expected) {
1192
+ throw new Error("Computer clipboard did not accept the exact pasted text");
1193
+ }
1194
+ }
1195
+
1196
+ function ComputerViewportFallback(props: {
1197
+ observation: ComputerObservation | null;
1198
+ machineLocked: boolean;
1199
+ connectionState: string;
1200
+ error: Error | null;
1201
+ onAction: (action: ComputerAction) => void;
1202
+ onReconnect: () => void;
1203
+ }) {
1204
+ if (props.machineLocked) {
1205
+ return (
1206
+ <div className="absolute inset-0 grid place-items-center bg-og-bg p-6">
1207
+ <div className="max-w-sm text-center">
1208
+ <LockKeyholeIcon className="mx-auto size-6 text-og-muted" />
1209
+ <p className="mt-3 text-og-menu font-medium text-og-fg">Machine locked</p>
1210
+ <p className="mt-1 text-og-control leading-5 text-og-muted">
1211
+ Live frames and input stay private until someone unlocks the machine.
1212
+ </p>
1213
+ </div>
1214
+ </div>
1215
+ );
1216
+ }
1217
+ const interactive = semanticNodes(props.observation)
1218
+ .filter((node) => semanticAction(node) !== null)
1219
+ .slice(0, 10);
1220
+ return (
1221
+ <div className="absolute inset-0 grid place-items-center bg-og-bg p-6">
1222
+ <div className="w-full max-w-md rounded-og-lg border border-og-border bg-og-surface-0 p-4 shadow-lg">
1223
+ <div className="flex items-center gap-2">
1224
+ {props.error ? (
1225
+ <CircleAlertIcon className="size-4 text-og-status-error" />
1226
+ ) : (
1227
+ <LoaderCircleIcon className="size-4 animate-spin text-og-muted" />
1228
+ )}
1229
+ <p className="text-og-menu font-medium text-og-fg">
1230
+ {props.error
1231
+ ? "Live view disconnected"
1232
+ : computerConnectionLabel(props.connectionState)}
1233
+ </p>
1234
+ </div>
1235
+ {interactive.length > 0 ? (
1236
+ <div className="mt-3 border-t border-og-border pt-3">
1237
+ <p className="mb-2 text-og-xs text-og-subtle">Native controls remain available</p>
1238
+ <div className="flex flex-wrap gap-1.5">
1239
+ {interactive.map((node) => (
1240
+ <button
1241
+ key={node.ref}
1242
+ type="button"
1243
+ onClick={() => {
1244
+ const action = semanticAction(node);
1245
+ if (action) props.onAction(action);
1246
+ }}
1247
+ 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"
1248
+ >
1249
+ {node.name || node.role}
1250
+ </button>
1251
+ ))}
1252
+ </div>
1253
+ </div>
1254
+ ) : null}
1255
+ {props.error ? (
1256
+ <button
1257
+ type="button"
1258
+ onClick={props.onReconnect}
1259
+ className="mt-3 text-og-control font-medium text-og-accent hover:underline"
1260
+ >
1261
+ Reconnect
1262
+ </button>
1263
+ ) : null}
1264
+ </div>
1265
+ </div>
1266
+ );
1267
+ }
1268
+
1269
+ function ComputerSemanticPanel(props: {
1270
+ observation: ComputerObservation | null;
1271
+ mutating: boolean;
1272
+ onAction: (action: ComputerAction) => void;
1273
+ }) {
1274
+ const nodes = semanticNodes(props.observation).slice(0, 100);
1275
+ return (
1276
+ <aside className="w-64 shrink-0 overflow-y-auto border-l border-og-border bg-og-surface-0 p-2">
1277
+ <div className="mb-2 flex items-center gap-1.5 px-1 text-og-xs font-medium uppercase tracking-[0.1em] text-og-subtle">
1278
+ <KeyboardIcon className="size-3" /> Native controls
1279
+ </div>
1280
+ {nodes.length === 0 ? (
1281
+ <p className="px-1 py-2 text-og-control leading-5 text-og-muted">
1282
+ This view has no accessibility controls. Use the live image instead.
1283
+ </p>
1284
+ ) : (
1285
+ <div className="space-y-0.5">
1286
+ {nodes.map((node) => (
1287
+ <ComputerSemanticControl
1288
+ key={node.ref}
1289
+ node={node}
1290
+ mutating={props.mutating}
1291
+ onAction={props.onAction}
1292
+ />
1293
+ ))}
1294
+ </div>
1295
+ )}
1296
+ </aside>
1297
+ );
1298
+ }
1299
+
1300
+ function ComputerSemanticControl(props: {
1301
+ node: InteractionSemanticNode;
1302
+ mutating: boolean;
1303
+ onAction: (action: ComputerAction) => void;
1304
+ }) {
1305
+ const { node } = props;
1306
+ const editable = node.actions.includes("set_value");
1307
+ const action = semanticAction(node);
1308
+ const observedValue = semanticValue(node) ?? "";
1309
+ const [value, setValue] = useState(() => observedValue);
1310
+
1311
+ useEffect(() => {
1312
+ setValue(observedValue);
1313
+ }, [node.ref, observedValue]);
1314
+
1315
+ if (editable) {
1316
+ return (
1317
+ <form
1318
+ className="rounded-og-sm px-2 py-1.5 hover:bg-og-surface-2"
1319
+ onSubmit={(event) => {
1320
+ event.preventDefault();
1321
+ props.onAction({
1322
+ type: "semantic",
1323
+ locator: { kind: "ref", ref: node.ref },
1324
+ action: "set_value",
1325
+ value,
1326
+ });
1327
+ }}
1328
+ >
1329
+ <label className="block truncate text-[10px] uppercase text-og-subtle">
1330
+ {node.name || node.identifier || node.role}
1331
+ </label>
1332
+ <div className="mt-1 flex gap-1">
1333
+ <input
1334
+ value={value}
1335
+ disabled={props.mutating}
1336
+ onChange={(event) => setValue(event.currentTarget.value)}
1337
+ className="min-w-0 flex-1 rounded-og-sm border border-og-border bg-og-surface-1 px-1.5 py-1 text-og-control text-og-fg outline-hidden focus:border-og-accent"
1338
+ aria-label={`Set ${node.name || node.identifier || node.role}`}
1339
+ />
1340
+ <button
1341
+ type="submit"
1342
+ disabled={props.mutating}
1343
+ className="rounded-og-sm border border-og-border bg-og-surface-1 px-2 text-og-control font-medium text-og-fg transition hover:bg-og-surface-2 disabled:opacity-50"
1344
+ >
1345
+ Set
1346
+ </button>
1347
+ </div>
1348
+ </form>
1349
+ );
1350
+ }
1351
+
1352
+ return (
1353
+ <button
1354
+ type="button"
1355
+ disabled={!action || props.mutating}
1356
+ onClick={() => {
1357
+ if (action) props.onAction(action);
1358
+ }}
1359
+ className="flex w-full items-start gap-2 rounded-og-sm px-2 py-1.5 text-left transition hover:bg-og-surface-2 disabled:cursor-default disabled:hover:bg-transparent"
1360
+ >
1361
+ <span className="mt-0.5 text-[10px] uppercase text-og-subtle">{node.role}</span>
1362
+ <span className="min-w-0 flex-1 truncate text-og-control text-og-fg">
1363
+ {node.name || semanticValue(node) || node.identifier || "Unnamed control"}
1364
+ </span>
1365
+ </button>
1366
+ );
1367
+ }
1368
+
1369
+ function ComputerStatusBar(props: {
1370
+ session: ComputerSession | null;
1371
+ target: ComputerTarget | null;
1372
+ connectionState: string;
1373
+ refreshing: boolean;
1374
+ showControls: boolean;
1375
+ controlCount: number;
1376
+ onToggleControls: () => void;
1377
+ }) {
1378
+ const screen = props.target?.kind === "screen";
1379
+ return (
1380
+ <div className="flex h-8 shrink-0 items-center gap-2 border-t border-og-border bg-og-surface-0 px-2 text-og-xs text-og-subtle">
1381
+ <span
1382
+ className={cn(
1383
+ "size-1.5 rounded-full",
1384
+ props.connectionState === "live" ? "bg-og-status-running" : "bg-og-muted",
1385
+ )}
1386
+ />
1387
+ <span>
1388
+ {props.connectionState === "live" ? "Live" : computerConnectionLabel(props.connectionState)}
1389
+ </span>
1390
+ <span className="min-w-0 flex-1 truncate">
1391
+ {screen
1392
+ ? "Full screen · input may move pointer and focus"
1393
+ : props.session?.capabilities?.backgroundActions
1394
+ ? "Window · semantic controls stay in the background"
1395
+ : (props.target?.kind ?? "Computer")}
1396
+ </span>
1397
+ {screen ? <MousePointer2Icon className="size-3" aria-hidden /> : null}
1398
+ {props.refreshing ? <LoaderCircleIcon className="size-3 animate-spin" /> : null}
1399
+ <button
1400
+ type="button"
1401
+ onClick={props.onToggleControls}
1402
+ aria-pressed={props.showControls}
1403
+ className={cn(
1404
+ "rounded px-1.5 py-0.5 transition hover:bg-og-surface-2 hover:text-og-fg",
1405
+ props.showControls && "bg-og-surface-2 text-og-fg",
1406
+ )}
1407
+ >
1408
+ Controls {props.controlCount}
1409
+ </button>
1410
+ </div>
1411
+ );
1412
+ }
1413
+
1414
+ function ComputerNotice(props: { icon: ReactNode; text: string; className?: string }) {
1415
+ return (
1416
+ <div
1417
+ className={cn(
1418
+ "grid h-full place-items-center bg-og-bg text-og-control text-og-muted",
1419
+ props.className,
1420
+ )}
1421
+ >
1422
+ <div className="flex items-center gap-2">
1423
+ {props.icon}
1424
+ {props.text}
1425
+ </div>
1426
+ </div>
1427
+ );
1428
+ }
1429
+
1430
+ function semanticNodes(observation: ComputerObservation | null): InteractionSemanticNode[] {
1431
+ if (observation?.semantic?.kind !== "snapshot") return [];
1432
+ const result: InteractionSemanticNode[] = [];
1433
+ const visit = (node: InteractionSemanticNode) => {
1434
+ result.push(node);
1435
+ node.children?.forEach(visit);
1436
+ };
1437
+ observation.semantic.roots.forEach(visit);
1438
+ return result;
1439
+ }
1440
+
1441
+ function semanticAction(node: InteractionSemanticNode): ComputerAction | null {
1442
+ const invoke = ["invoke", "press", "click"].some((action) => node.actions.includes(action));
1443
+ if (invoke) {
1444
+ return {
1445
+ type: "semantic",
1446
+ locator: { kind: "ref", ref: node.ref },
1447
+ action: "invoke",
1448
+ };
1449
+ }
1450
+ // A row in the background-native controls panel must never become an
1451
+ // implicit foregrounding gesture. Focus remains available through the typed
1452
+ // SDK/tool action when the caller explicitly intends to take the shared seat.
1453
+ return null;
1454
+ }
1455
+
1456
+ function semanticValue(node: InteractionSemanticNode): string | null {
1457
+ return typeof node.value === "string" ? node.value : null;
1458
+ }
1459
+
1460
+ function paintCanvas(
1461
+ canvas: HTMLCanvasElement,
1462
+ image: CanvasImageSource,
1463
+ width: number,
1464
+ height: number,
1465
+ ): void {
1466
+ canvas.width = width;
1467
+ canvas.height = height;
1468
+ const context = canvas.getContext("2d", { alpha: false });
1469
+ if (!context) throw new Error("Computer canvas is unavailable.");
1470
+ context.drawImage(image, 0, 0, width, height);
1471
+ }
1472
+
1473
+ function loadImage(url: string): Promise<HTMLImageElement> {
1474
+ return new Promise((resolve, reject) => {
1475
+ const image = new Image();
1476
+ image.onload = () => resolve(image);
1477
+ image.onerror = () => reject(new Error("Computer frame image could not be decoded."));
1478
+ image.src = url;
1479
+ });
1480
+ }
1481
+
1482
+ function computerPoint(
1483
+ canvas: HTMLCanvasElement | null,
1484
+ frame: ComputerFrame,
1485
+ clientX: number,
1486
+ clientY: number,
1487
+ ): { x: number; y: number } | null {
1488
+ if (!canvas) return null;
1489
+ const rect = canvas.getBoundingClientRect();
1490
+ if (rect.width <= 0 || rect.height <= 0) return null;
1491
+ return {
1492
+ x: Math.max(0, Math.min(frame.width - 1, ((clientX - rect.left) / rect.width) * frame.width)),
1493
+ y: Math.max(0, Math.min(frame.height - 1, ((clientY - rect.top) / rect.height) * frame.height)),
1494
+ };
1495
+ }
1496
+
1497
+ export function computerKey(
1498
+ event: Pick<
1499
+ KeyboardEvent<HTMLTextAreaElement>,
1500
+ "altKey" | "ctrlKey" | "key" | "metaKey" | "shiftKey"
1501
+ >,
1502
+ ): string | null {
1503
+ if (["Alt", "AltGraph", "Control", "Meta", "Shift"].includes(event.key)) return null;
1504
+ const special = new Set([
1505
+ "Enter",
1506
+ "Tab",
1507
+ "Escape",
1508
+ "Backspace",
1509
+ "Delete",
1510
+ "ArrowUp",
1511
+ "ArrowDown",
1512
+ "ArrowLeft",
1513
+ "ArrowRight",
1514
+ "Home",
1515
+ "End",
1516
+ "PageUp",
1517
+ "PageDown",
1518
+ ]);
1519
+ const modified = event.altKey || event.ctrlKey || event.metaKey;
1520
+ if (!modified && !special.has(event.key)) return null;
1521
+ const parts: string[] = [];
1522
+ if (event.ctrlKey) parts.push("Control");
1523
+ if (event.altKey) parts.push("Alt");
1524
+ if (event.metaKey) parts.push("Meta");
1525
+ if (event.shiftKey) parts.push("Shift");
1526
+ parts.push(event.key === " " ? "Space" : event.key);
1527
+ return parts.join("+");
1528
+ }
1529
+
1530
+ function sameFrameFence(left: ComputerFrame, right: ComputerFrame): boolean {
1531
+ return (
1532
+ left.frameId === right.frameId &&
1533
+ left.computerSessionId === right.computerSessionId &&
1534
+ left.targetId === right.targetId &&
1535
+ left.targetGeneration === right.targetGeneration
1536
+ );
1537
+ }
1538
+
1539
+ function isLiveComputer(session: ComputerSession): boolean {
1540
+ return !["ending", "ended", "failed", "lost"].includes(session.lifecycle);
1541
+ }
1542
+
1543
+ function computerFailureMessage(session: ComputerSession): string | null {
1544
+ switch (session.failureCode) {
1545
+ case "machine_locked":
1546
+ return "Unlock the connected Mac, then try again.";
1547
+ case "controller_heartbeat_expired":
1548
+ return "The computer connection was lost. Try opening it again.";
1549
+ case null:
1550
+ return null;
1551
+ default:
1552
+ return "The computer could not be opened. Try again.";
1553
+ }
1554
+ }
1555
+
1556
+ function countInterventions(
1557
+ interventions: readonly InteractionIntervention[],
1558
+ ): Map<string, number> {
1559
+ const counts = new Map<string, number>();
1560
+ for (const intervention of interventions) {
1561
+ counts.set(intervention.resourceId, (counts.get(intervention.resourceId) ?? 0) + 1);
1562
+ }
1563
+ return counts;
1564
+ }
1565
+
1566
+ function interventionTitle(intervention: InteractionIntervention): string {
1567
+ switch (intervention.kind) {
1568
+ case "manual_login":
1569
+ return "Sign in needed";
1570
+ case "mfa":
1571
+ return "Verification needed";
1572
+ case "external_action":
1573
+ case "confirmation":
1574
+ return "Action needed";
1575
+ case "other":
1576
+ return "Computer needs your help";
1577
+ }
1578
+ }
1579
+
1580
+ function platformLabel(session: ComputerSession): string {
1581
+ switch (session.platform) {
1582
+ case "macos":
1583
+ return "Mac";
1584
+ case "linux":
1585
+ return "Linux";
1586
+ case "windows":
1587
+ return "Windows";
1588
+ default:
1589
+ return "Computer";
1590
+ }
1591
+ }
1592
+
1593
+ function placementLabel(session: ComputerSession): string {
1594
+ switch (session.placement.kind) {
1595
+ case "sandbox_group":
1596
+ return "Agent sandbox";
1597
+ case "connected_machine":
1598
+ return "Connected machine";
1599
+ case "attached_device":
1600
+ return "Your machine";
1601
+ case "external_provider":
1602
+ return "Cloud computer";
1603
+ }
1604
+ }
1605
+
1606
+ function lifecycleLabel(lifecycle: ComputerSession["lifecycle"]): string {
1607
+ switch (lifecycle) {
1608
+ case "starting":
1609
+ return "Opening computer…";
1610
+ case "suspending":
1611
+ return "Pausing computer…";
1612
+ case "suspended":
1613
+ return "Computer paused";
1614
+ case "restoring":
1615
+ return "Restoring computer…";
1616
+ case "ending":
1617
+ return "Closing computer…";
1618
+ default:
1619
+ return "Connecting to computer…";
1620
+ }
1621
+ }
1622
+
1623
+ function computerConnectionLabel(state: string): string {
1624
+ switch (state) {
1625
+ case "attaching":
1626
+ return "Opening computer view…";
1627
+ case "connecting":
1628
+ return "Connecting…";
1629
+ case "reconnecting":
1630
+ return "Reconnecting…";
1631
+ case "error":
1632
+ return "Disconnected";
1633
+ default:
1634
+ return "Waiting for computer…";
1635
+ }
1636
+ }