@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
@@ -4,8 +4,16 @@ import type {
4
4
  DesktopStreamCapability,
5
5
  } from "@opengeni/sdk";
6
6
  import { LoaderCircleIcon, MonitorIcon, MousePointerClickIcon, WifiOffIcon } from "lucide-react";
7
- import { type ReactNode, useEffect, useRef, useState } from "react";
7
+ import {
8
+ type ClipboardEvent,
9
+ type KeyboardEvent as ReactKeyboardEvent,
10
+ type ReactNode,
11
+ useEffect,
12
+ useRef,
13
+ useState,
14
+ } from "react";
8
15
  import { cn } from "../lib/cn";
16
+ import { interactionHostPlatform, type InteractionHostPlatform } from "../lib/host-platform";
9
17
  import { useDesktopStream } from "../hooks/use-desktop-stream";
10
18
  import type { DesktopWebSocketFactory } from "../hooks/use-relay-frame-stream";
11
19
 
@@ -24,26 +32,43 @@ export type DesktopViewerProps = {
24
32
  scaleViewport?: boolean | undefined;
25
33
  /** Custom RFB factory (tests / a WebRTC swap). Defaults to lazy @novnc/novnc. */
26
34
  rfbFactory?: DesktopRfbFactory | undefined;
35
+ /** Authenticated controller protocols for proxied RFB transports. */
36
+ webSocketProtocols?: string[] | undefined;
27
37
  /** Custom socket factory for the self-hosted `relay-frames` transport (tests).
28
38
  * Defaults to `new WebSocket(url)`. Mirrors `rfbFactory`. */
29
39
  webSocketFactory?: DesktopWebSocketFactory | undefined;
30
40
  /**
31
- * Consent gate for the un-redacted (and possibly shared) pixel plane. Rendered
32
- * BEFORE connecting whenever the desktop requires acknowledgment that hasn't
33
- * been given. Call `onAccept` to record consent (the host wires it to
34
- * `client.acknowledgeStream` + a re-negotiate). When omitted, a default
35
- * banner is shown.
41
+ * Optional host-owned paste path. ComputerSession hosts use their native,
42
+ * target-scoped clipboard action instead of relying on an RFB server to sync
43
+ * ClientCutText into the graphical seat. Return true once the host accepted
44
+ * the text. Without a host authority the viewer leaves paste untouched rather
45
+ * than claiming success for an unreliable display-server clipboard heuristic.
46
+ */
47
+ onPasteText?: ((text: string) => boolean) | undefined;
48
+ /** OS behind the RFB seat. Used only to translate the host's primary shortcut
49
+ * (Cmd on macOS, Ctrl elsewhere) to the target's primary modifier. */
50
+ targetPlatform?: "linux" | "macos" | "windows" | null | undefined;
51
+ /**
52
+ * @deprecated Desktop acknowledgment is automatic. Retained as an ignored
53
+ * compatibility prop so existing embedders keep compiling.
36
54
  */
37
55
  renderConsentGate?: ((onAccept: () => void, shared: boolean) => ReactNode) | undefined;
38
- /** Called when the default consent gate's accept button is pressed. */
39
- onAcknowledge?: (() => void) | undefined;
40
56
  /**
41
- * Whether the host has the viewer attach engaged (i.e. the user has opted into
42
- * watching the parent's `watchDesktop`/`attachDesktop`). Drives the
43
- * cold-state behaviour: when watching, a cold-but-warmable lease AUTO-WARMS
44
- * (and re-warms when the box drains) instead of dead-ending. When omitted we
45
- * infer it from a recorded consent (the default gate's accept), so the
46
- * component still self-heals after the first acknowledgment.
57
+ * Called once when the viewer mounts. Hosts use this to engage the desktop
58
+ * holder immediately when the Desktop tab opens.
59
+ */
60
+ onActivate?: (() => void) | undefined;
61
+ /**
62
+ * Called automatically when the server requires acknowledgment of the
63
+ * un-redacted (and possibly shared) pixel plane. The viewer shows its ordinary
64
+ * opening state while this completes; no click-through gate is rendered.
65
+ */
66
+ onAcknowledge?: (() => void | Promise<void>) | undefined;
67
+ /**
68
+ * Whether the host has the viewer attach engaged. Drives the cold-state
69
+ * behaviour: when watching, a cold-but-warmable lease AUTO-WARMS (and re-warms
70
+ * when the box drains) instead of dead-ending. Mounting the viewer counts as
71
+ * watching when this is omitted.
47
72
  */
48
73
  watching?: boolean | undefined;
49
74
  /**
@@ -51,8 +76,7 @@ export type DesktopViewerProps = {
51
76
  * already been recorded — only the box drained). The host wires this to
52
77
  * "engage the viewer attach + re-negotiate". Called automatically when a
53
78
  * watched desktop is found cold-but-warmable, and behind the manual retry on
54
- * the warming notice. Distinct from `onAcknowledge`, which is the FIRST,
55
- * consent-bearing warm.
79
+ * the warming notice. Distinct from the automatic acknowledgment request.
56
80
  */
57
81
  onWarm?: (() => void) | undefined;
58
82
  /** Shown when transport is null (headless backend / degraded / disabled). */
@@ -75,13 +99,14 @@ export type DesktopViewerProps = {
75
99
  /**
76
100
  * The derived desktop surface state. A single source of truth so the overlay,
77
101
  * the scrim, the auto-warm effect, and the watchdog all agree. Priority order
78
- * (highest first): viewer-cap → unavailable → consent → warming → connect-failed
79
- * → error → connecting → connected.
102
+ * (highest first): viewer-cap → unavailable → acknowledgment-error → warming →
103
+ * connect-failed → error → opening/connecting → connected.
80
104
  */
81
105
  type DesktopUiState =
82
106
  | "viewer_cap" // 429 — the per-session live-viewer limit is reached.
83
107
  | "unavailable" // genuinely unsupported (headless/policy/os/backend/not-provisioned).
84
- | "consent" // un-redacted/shared plane needs acknowledgment first.
108
+ | "acknowledgment_error" // automatic un-redacted/shared acknowledgment failed.
109
+ | "opening" // acknowledgment or viewer attach is in flight.
85
110
  | "warming" // cold-but-warmable: box not running yet (we auto-warm + spin).
86
111
  | "connecting" // url is live, RFB negotiating/handshaking (we spin, with a watchdog).
87
112
  | "connect_failed" // watchdog fired: url present but never connected.
@@ -122,10 +147,11 @@ function isHardUnavailable(reason: CapabilityUnavailableReason | null): boolean
122
147
  * reason).
123
148
  *
124
149
  * Warming: a cold-but-warmable lease (`reason: "lease_cold"`) is NOT a dead end.
125
- * When the user is watching (consented), the viewer asks the host to (re)warm
126
- * the box (`onWarm`) and shows a "Warming…" spinner; if the box later drains to
127
- * cold it re-warms. Genuinely-unavailable surfaces (headless/policy/os/backend)
128
- * keep a clear, static unavailable notice.
150
+ * When the viewer is mounted, it asks the host to engage the desktop and records
151
+ * any required acknowledgment automatically. A cold-but-warmable box then
152
+ * (re)warms through `onWarm`; if it later drains to cold it re-warms.
153
+ * Genuinely-unavailable surfaces (headless/policy/os/backend) keep a clear,
154
+ * static unavailable notice.
129
155
  */
130
156
  export function DesktopViewer({
131
157
  capability,
@@ -133,8 +159,11 @@ export function DesktopViewer({
133
159
  showControlToggle = true,
134
160
  scaleViewport,
135
161
  rfbFactory,
162
+ webSocketProtocols,
136
163
  webSocketFactory,
137
- renderConsentGate,
164
+ onPasteText,
165
+ targetPlatform,
166
+ onActivate,
138
167
  onAcknowledge,
139
168
  watching,
140
169
  onWarm,
@@ -146,7 +175,13 @@ export function DesktopViewer({
146
175
  className,
147
176
  }: DesktopViewerProps) {
148
177
  const containerRef = useRef<HTMLDivElement | null>(null);
149
- const [consented, setConsented] = useState(false);
178
+ const onActivateRef = useRef(onActivate);
179
+ const onAcknowledgeRef = useRef(onAcknowledge);
180
+ onActivateRef.current = onActivate;
181
+ onAcknowledgeRef.current = onAcknowledge;
182
+ const acknowledgmentAttemptedRef = useRef(false);
183
+ const [acknowledgmentError, setAcknowledgmentError] = useState<Error | null>(null);
184
+ const [acknowledgmentRetryNonce, setAcknowledgmentRetryNonce] = useState(0);
150
185
  // Local control state when not externally controlled. The server gate
151
186
  // (`capability.mode`) is the hard ceiling — a read-only deployment can never
152
187
  // be flipped to interactive regardless of this toggle.
@@ -156,16 +191,16 @@ export function DesktopViewer({
156
191
  const wantControl = externallyControlled ? interactive : takeControl;
157
192
  const inControl = Boolean(wantControl) && serverAllowsControl;
158
193
 
159
- // Whether the host has the viewer attach engaged. Prefer the explicit prop;
160
- // fall back to a locally-recorded consent so the component still self-heals
161
- // (auto-warms / re-warms) once the user has accepted the gate at least once.
162
- const isWatching = watching ?? consented;
194
+ // Mounting this component means the Desktop surface is being viewed. A host
195
+ // may still pass its holder state explicitly while the activation callback
196
+ // flips that state on the first mount.
197
+ const isWatching = watching ?? true;
163
198
 
164
199
  // ── Decide the rendered state (before touching the stream hook) ─────────────
165
200
  const transportNull = !capability || capability.transport === null;
166
201
  const reason = capability?.reason ?? null;
167
- const needsAck =
168
- capability?.requiresAcknowledgment === true && capability.acknowledged !== true && !consented;
202
+ const needsAcknowledgment =
203
+ capability?.requiresAcknowledgment === true && capability.acknowledged !== true;
169
204
  // No live address yet on an otherwise-live transport (post-ack, mid-warm).
170
205
  const noLiveAddress = Boolean(capability) && !transportNull && !capability!.url;
171
206
 
@@ -176,9 +211,45 @@ export function DesktopViewer({
176
211
  // Genuinely-unavailable: transport null for a HARD reason (not lease_cold).
177
212
  const hardUnavailable = transportNull && isHardUnavailable(reason);
178
213
 
179
- const accept = () => {
180
- setConsented(true);
181
- onAcknowledge?.();
214
+ // Selecting Desktop mounts the viewer, which is itself the user's intent to
215
+ // open it. Engage the holder immediately — no separate "watch" click.
216
+ useEffect(() => {
217
+ onActivateRef.current?.();
218
+ }, []);
219
+
220
+ // Preserve the server-side acknowledgment/audit contract without making the
221
+ // user click through a disclosure card. One automatic attempt runs for each
222
+ // unacknowledged episode; a genuine request failure gets an explicit retry.
223
+ useEffect(() => {
224
+ if (!needsAcknowledgment) {
225
+ acknowledgmentAttemptedRef.current = false;
226
+ setAcknowledgmentError(null);
227
+ return;
228
+ }
229
+ if (acknowledgmentAttemptedRef.current) return;
230
+ acknowledgmentAttemptedRef.current = true;
231
+ setAcknowledgmentError(null);
232
+ const acknowledge = onAcknowledgeRef.current;
233
+ if (!acknowledge) {
234
+ setAcknowledgmentError(new Error("Desktop acknowledgment is not configured."));
235
+ return;
236
+ }
237
+ let active = true;
238
+ void Promise.resolve()
239
+ .then(() => acknowledge())
240
+ .catch((cause) => {
241
+ if (!active) return;
242
+ setAcknowledgmentError(cause instanceof Error ? cause : new Error(String(cause)));
243
+ });
244
+ return () => {
245
+ active = false;
246
+ };
247
+ }, [needsAcknowledgment, acknowledgmentRetryNonce]);
248
+
249
+ const retryAcknowledgment = () => {
250
+ acknowledgmentAttemptedRef.current = false;
251
+ setAcknowledgmentError(null);
252
+ setAcknowledgmentRetryNonce((nonce) => nonce + 1);
182
253
  };
183
254
 
184
255
  // Release control WITHOUT ever swallowing a key the desktop needs. Esc, and
@@ -218,13 +289,15 @@ export function DesktopViewer({
218
289
  // Do NOT open a socket while the viewer-cap (429) notice is showing — the slot
219
290
  // is already exhausted, so connecting would only burn a doomed attempt (and in
220
291
  // tests leak an unhandled ws error from the never-resolving tunnel URL).
221
- const connectCapability = !transportNull && !needsAck && !viewerCapReached ? capability : null;
292
+ const connectCapability =
293
+ !transportNull && !needsAcknowledgment && !viewerCapReached ? capability : null;
222
294
  const stream = useDesktopStream({
223
295
  capability: connectCapability,
224
296
  containerRef,
225
297
  interactive: inControl,
226
298
  ...(scaleViewport !== undefined ? { scaleViewport } : {}),
227
299
  ...(rfbFactory ? { rfbFactory } : {}),
300
+ ...(webSocketProtocols ? { webSocketProtocols } : {}),
228
301
  ...(webSocketFactory ? { webSocketFactory } : {}),
229
302
  });
230
303
 
@@ -238,21 +311,19 @@ export function DesktopViewer({
238
311
 
239
312
  // ── AUTO-WARM ───────────────────────────────────────────────────────────────
240
313
  // When the user is watching and the desktop is cold-but-warmable, ask the host
241
- // to (re)warm the box. This covers BOTH (a) the user just accepted consent and
314
+ // to (re)warm the box. This covers BOTH (a) the desktop was just opened and
242
315
  // (b) a previously-warm box that drained back to cold under a live viewer. We
243
316
  // fire once per distinct cold episode (keyed on the lease epoch + url) so we
244
317
  // don't spam the attach while a single warm is in flight. `onWarm` is the
245
- // no-re-ack path; if the host only wired `onAcknowledge` (no `onWarm`), the
246
- // first warm still rides consent and subsequent drains fall back to it.
318
+ // no-re-ack path; acknowledgment remains a separate automatic request.
247
319
  const warmKeyRef = useRef<string | null>(null);
248
320
  useEffect(() => {
249
- if (!isWatching || !coldWarmable || needsAck || viewerCapReached) {
321
+ if (!isWatching || !coldWarmable || needsAcknowledgment || viewerCapReached) {
250
322
  // Reset the de-dupe once we leave the cold episode so a future drain warms.
251
323
  if (!coldWarmable) warmKeyRef.current = null;
252
324
  return;
253
325
  }
254
- const warm = onWarm ?? onAcknowledge;
255
- if (!warm) return;
326
+ if (!onWarm) return;
256
327
  // Key the de-dupe on the cell fields available to the viewer: while cold,
257
328
  // `reason`/`url`/`expiresAt` are stable, so we warm exactly once; once the box
258
329
  // warms the cell changes (url+expiresAt minted) and `coldWarmable` flips false,
@@ -260,10 +331,10 @@ export function DesktopViewer({
260
331
  const key = `${capability?.reason ?? ""}:${capability?.url ?? ""}:${capability?.expiresAt ?? ""}`;
261
332
  if (warmKeyRef.current === key) return; // already kicked this episode.
262
333
  warmKeyRef.current = key;
263
- warm();
334
+ onWarm();
264
335
  // capability identity is the trigger; leaseEpoch/expiresAt key the episode.
265
336
  // eslint-disable-next-line react-hooks/exhaustive-deps
266
- }, [isWatching, coldWarmable, needsAck, viewerCapReached, capability, onWarm, onAcknowledge]);
337
+ }, [isWatching, coldWarmable, needsAcknowledgment, viewerCapReached, capability, onWarm]);
267
338
 
268
339
  // ── TAB RE-ATTACH ───────────────────────────────────────────────────────────
269
340
  // The "stuck Watching, never connects" bug: the RFB socket never (re)fires when
@@ -327,11 +398,51 @@ export function DesktopViewer({
327
398
  stream.reconnect();
328
399
  };
329
400
 
401
+ const paste = (event: ClipboardEvent<HTMLDivElement>) => {
402
+ if (!inControl) return;
403
+ const text = event.clipboardData.getData("text/plain");
404
+ if (!text && !event.clipboardData.types.includes("text/plain")) return;
405
+ if (!onPasteText?.(text)) return;
406
+ event.preventDefault();
407
+ event.stopPropagation();
408
+ };
409
+
410
+ const primaryShortcut = (event: ReactKeyboardEvent<HTMLDivElement>) => {
411
+ if (!inControl || !targetPlatform) return;
412
+ const hostPlatform = interactionHostPlatform();
413
+ // noVNC maps a Mac Command key to remote Alt before the actual chord key
414
+ // arrives. Suppress that standalone physical modifier; the complete target
415
+ // chord below owns both its down and up events.
416
+ if (isHostPrimaryModifier(event, hostPlatform)) {
417
+ event.stopPropagation();
418
+ return;
419
+ }
420
+ const chord = desktopPrimaryShortcut(event, hostPlatform, targetPlatform);
421
+ if (!chord) return;
422
+ // Copy/paste must keep the browser's default clipboard dispatch alive, but
423
+ // noVNC must not also send the host modifier into the remote seat.
424
+ if (chord === "clipboard") {
425
+ event.stopPropagation();
426
+ return;
427
+ }
428
+ const sent = chord.every(({ keysym, code, down }) => stream.sendKey?.(keysym, code, down));
429
+ if (!sent) return;
430
+ event.preventDefault();
431
+ event.stopPropagation();
432
+ };
433
+
434
+ const primaryModifierUp = (event: ReactKeyboardEvent<HTMLDivElement>) => {
435
+ if (inControl && isHostPrimaryModifier(event, interactionHostPlatform())) {
436
+ event.stopPropagation();
437
+ }
438
+ };
439
+
330
440
  // ── Resolve the single UI state (priority order) ────────────────────────────
331
441
  let uiState: DesktopUiState;
332
442
  if (viewerCapReached) uiState = "viewer_cap";
333
443
  else if (hardUnavailable) uiState = "unavailable";
334
- else if (needsAck && !isWatching) uiState = "consent";
444
+ else if (acknowledgmentError) uiState = "acknowledgment_error";
445
+ else if (needsAcknowledgment) uiState = "opening";
335
446
  else if (coldWarmable) uiState = "warming";
336
447
  else if (connected) uiState = "connected";
337
448
  else if (stream.error) uiState = "error";
@@ -340,7 +451,7 @@ export function DesktopViewer({
340
451
 
341
452
  // An overlay blocks the surface for every state except connected; the toggle
342
453
  // and the live scrim are suppressed under an overlay.
343
- const overlayShown = uiState !== "connected" && uiState !== "connecting";
454
+ const overlayShown = uiState !== "connected" && uiState !== "connecting" && uiState !== "opening";
344
455
  const showToggle = showControlToggle && !overlayShown;
345
456
 
346
457
  let overlay: ReactNode = null;
@@ -358,11 +469,11 @@ export function DesktopViewer({
358
469
  renderUnavailable?.(reason) ??
359
470
  defaultNotice("Desktop unavailable", unavailableCopy(reason));
360
471
  break;
361
- case "consent":
362
- overlay = renderConsentGate ? (
363
- renderConsentGate(accept, capability?.shared ?? false)
364
- ) : (
365
- <DefaultConsentGate shared={capability?.shared ?? false} onAccept={accept} />
472
+ case "acknowledgment_error":
473
+ overlay = defaultNotice(
474
+ "Couldn’t open desktop",
475
+ acknowledgmentError?.message ?? "The desktop acknowledgment failed.",
476
+ retryAcknowledgment,
366
477
  );
367
478
  break;
368
479
  case "warming":
@@ -382,6 +493,7 @@ export function DesktopViewer({
382
493
  reconnect,
383
494
  );
384
495
  break;
496
+ case "opening":
385
497
  case "connecting":
386
498
  case "connected":
387
499
  overlay = null;
@@ -400,6 +512,10 @@ export function DesktopViewer({
400
512
  data-state={stream.state}
401
513
  data-ui-state={uiState}
402
514
  data-in-control={inControl ? "true" : undefined}
515
+ data-target-platform={targetPlatform ?? undefined}
516
+ onKeyDownCapture={primaryShortcut}
517
+ onKeyUpCapture={primaryModifierUp}
518
+ onPasteCapture={paste}
403
519
  >
404
520
  {/* noVNC mount target. It appends a `width:100%;height:100%` `_screen` div
405
521
  and AUTOSCALES the 1280x800 framebuffer to fit THIS box. We pin it to
@@ -420,7 +536,7 @@ export function DesktopViewer({
420
536
  rectangle before the first framebuffer paints. Only shown in the
421
537
  `connecting` UI-state (every other non-connected state has an explicit
422
538
  overlay). A spinner + transitional copy makes it feel live. */}
423
- {uiState === "connecting" && (
539
+ {(uiState === "opening" || uiState === "connecting") && (
424
540
  <div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-3 text-og-fg-subtle">
425
541
  <span className="relative flex items-center justify-center">
426
542
  <MonitorIcon className="size-8 opacity-30" strokeWidth={1.5} />
@@ -429,7 +545,9 @@ export function DesktopViewer({
429
545
  strokeWidth={1.25}
430
546
  />
431
547
  </span>
432
- <span className="text-og-sm">Connecting to the desktop…</span>
548
+ <span className="text-og-sm">
549
+ {uiState === "opening" ? "Opening the desktop…" : "Connecting to the desktop…"}
550
+ </span>
433
551
  </div>
434
552
  )}
435
553
 
@@ -474,6 +592,51 @@ export function DesktopViewer({
474
592
  );
475
593
  }
476
594
 
595
+ type RfbKeyEvent = { keysym: number; code: string; down: boolean };
596
+
597
+ function isHostPrimaryModifier(
598
+ event: Pick<ReactKeyboardEvent<HTMLDivElement>, "key">,
599
+ hostPlatform: InteractionHostPlatform,
600
+ ): boolean {
601
+ return event.key === (hostPlatform === "mac" ? "Meta" : "Control");
602
+ }
603
+
604
+ /** Translate one host-primary chord into deterministic RFB events for the
605
+ * target OS. noVNC otherwise treats a Mac Command key as remote Alt, which
606
+ * makes ordinary Cmd+A/Z shortcuts silently wrong on Linux sandboxes. */
607
+ export function desktopPrimaryShortcut(
608
+ event: Pick<
609
+ ReactKeyboardEvent<HTMLDivElement>,
610
+ "altKey" | "ctrlKey" | "key" | "metaKey" | "shiftKey" | "code"
611
+ >,
612
+ hostPlatform: InteractionHostPlatform,
613
+ targetPlatform: "linux" | "macos" | "windows",
614
+ ): RfbKeyEvent[] | "clipboard" | null {
615
+ const primaryHeld = hostPlatform === "mac" ? event.metaKey : event.ctrlKey;
616
+ if (!primaryHeld || [...event.key].length !== 1) return null;
617
+ const key = event.key.toLowerCase();
618
+ if (!event.altKey && (key === "c" || key === "v")) return "clipboard";
619
+ const primary =
620
+ targetPlatform === "macos"
621
+ ? { keysym: 0xffeb, code: "MetaLeft" }
622
+ : { keysym: 0xffe3, code: "ControlLeft" };
623
+ const modifiers = [
624
+ primary,
625
+ ...(event.altKey ? [{ keysym: 0xffe9, code: "AltLeft" }] : []),
626
+ ...(event.shiftKey ? [{ keysym: 0xffe1, code: "ShiftLeft" }] : []),
627
+ ];
628
+ const keyEvent = {
629
+ keysym: key.codePointAt(0)!,
630
+ code: event.code || (/^[a-z]$/u.test(key) ? `Key${key.toUpperCase()}` : key),
631
+ };
632
+ return [
633
+ ...modifiers.map((modifier) => ({ ...modifier, down: true })),
634
+ { ...keyEvent, down: true },
635
+ { ...keyEvent, down: false },
636
+ ...modifiers.reverse().map((modifier) => ({ ...modifier, down: false })),
637
+ ];
638
+ }
639
+
477
640
  /**
478
641
  * The primary WATCHING-state call-to-action: a large, centered pill button
479
642
  * overlaid on the desktop inviting the viewer to drive the mouse & keyboard.
@@ -630,28 +793,6 @@ function WarmingNotice({ onRetry }: { onRetry?: (() => void) | undefined }) {
630
793
  );
631
794
  }
632
795
 
633
- function DefaultConsentGate({ shared, onAccept }: { shared: boolean; onAccept: () => void }) {
634
- return (
635
- <div className="max-w-sm rounded-og-lg border border-og-border bg-og-bg p-4 text-center text-og-base text-og-fg">
636
- <div className="mb-1 font-medium">Watch the live desktop?</div>
637
- <p className="mb-3 text-og-sm text-og-fg-subtle">
638
- The desktop pixel stream is <strong>un-redacted</strong> — it can show secrets the agent
639
- prints on screen.
640
- {shared
641
- ? " This sandbox is shared: you'll also see other sessions' agents on the same screen."
642
- : ""}
643
- </p>
644
- <button
645
- type="button"
646
- onClick={onAccept}
647
- className="rounded-og-sm bg-og-accent px-3 py-1.5 text-og-sm font-medium text-og-accent-fg pointer-coarse:min-h-11"
648
- >
649
- I understand — show the desktop
650
- </button>
651
- </div>
652
- );
653
- }
654
-
655
796
  function defaultNotice(title: string, body: string, onRetry?: () => void): ReactNode {
656
797
  return (
657
798
  <div className="max-w-sm rounded-og-lg border border-og-border bg-og-bg p-4 text-center text-og-base text-og-fg">
@@ -0,0 +1,89 @@
1
+ import type { GeneratedVideoReceipt, VideoArtifactPlaybackSource } from "@opengeni/sdk";
2
+ import { useEffect, useState } from "react";
3
+ import { cn } from "../lib/cn";
4
+ import type { VideoArtifactPlaybackLoader } from "../timeline";
5
+
6
+ export type GeneratedVideoPlayerProps = {
7
+ receipt: GeneratedVideoReceipt;
8
+ loadPlaybackSource: VideoArtifactPlaybackLoader;
9
+ className?: string | undefined;
10
+ label?: string | undefined;
11
+ };
12
+
13
+ type SourceState =
14
+ | { kind: "loading" }
15
+ | { kind: "ready"; source: VideoArtifactPlaybackSource }
16
+ | { kind: "error"; message: string };
17
+
18
+ /** Native, Range-based playback. Video bytes never pass through React or the SDK heap. */
19
+ export function GeneratedVideoPlayer({
20
+ receipt,
21
+ loadPlaybackSource,
22
+ className,
23
+ label = "Generated video",
24
+ }: GeneratedVideoPlayerProps) {
25
+ const [retry, setRetry] = useState(0);
26
+ const [state, setState] = useState<SourceState>({ kind: "loading" });
27
+
28
+ useEffect(() => {
29
+ const controller = new AbortController();
30
+ setState({ kind: "loading" });
31
+ void loadPlaybackSource(receipt.artifact.artifactId, controller.signal).then(
32
+ (source) => {
33
+ if (!controller.signal.aborted) setState({ kind: "ready", source });
34
+ },
35
+ (error: unknown) => {
36
+ if (controller.signal.aborted) return;
37
+ setState({
38
+ kind: "error",
39
+ message: error instanceof Error ? error.message : "Playback is unavailable.",
40
+ });
41
+ },
42
+ );
43
+ return () => controller.abort();
44
+ }, [loadPlaybackSource, receipt.artifact.artifactId, retry]);
45
+
46
+ if (state.kind === "loading") {
47
+ return (
48
+ <div
49
+ aria-label={`Loading ${label.toLowerCase()}`}
50
+ className={cn(
51
+ "aspect-video w-full animate-pulse rounded-og-md bg-og-surface-2 motion-reduce:animate-none",
52
+ className,
53
+ )}
54
+ />
55
+ );
56
+ }
57
+ if (state.kind === "error") {
58
+ return (
59
+ <div
60
+ role="status"
61
+ className={cn(
62
+ "rounded-og-md border border-og-status-failed/30 bg-og-status-failed/5 px-3 py-2 text-og-sm text-og-status-failed",
63
+ className,
64
+ )}
65
+ >
66
+ {state.message}
67
+ </div>
68
+ );
69
+ }
70
+
71
+ return (
72
+ <video
73
+ key={state.source.url}
74
+ aria-label={label}
75
+ className={cn(
76
+ "max-h-[32rem] w-full rounded-og-md bg-black object-contain shadow-sm",
77
+ className,
78
+ )}
79
+ controls
80
+ playsInline
81
+ preload="metadata"
82
+ src={state.source.url}
83
+ onError={() => {
84
+ if (retry === 0) setRetry(1);
85
+ else setState({ kind: "error", message: "Video playback failed." });
86
+ }}
87
+ />
88
+ );
89
+ }
@@ -0,0 +1,99 @@
1
+ import type { InteractionIntervention } from "@opengeni/sdk/interaction";
2
+ import { CheckIcon, CircleAlertIcon, LoaderCircleIcon, XIcon } from "lucide-react";
3
+ import { cn } from "../lib/cn";
4
+
5
+ export type InteractionInterventionBannerProps = {
6
+ interventions: InteractionIntervention[];
7
+ activeTargetId?: string | null | undefined;
8
+ mutating?: boolean | undefined;
9
+ className?: string | undefined;
10
+ onOpen?: ((intervention: InteractionIntervention) => void) | undefined;
11
+ onResolve: (intervention: InteractionIntervention, outcome: "completed" | "dismissed") => void;
12
+ };
13
+
14
+ /** Compact durable human-action prompt shared by Browser and Computer viewers. */
15
+ export function InteractionInterventionBanner({
16
+ interventions,
17
+ activeTargetId,
18
+ mutating = false,
19
+ className,
20
+ onOpen,
21
+ onResolve,
22
+ }: InteractionInterventionBannerProps) {
23
+ const intervention = interventions[0];
24
+ if (!intervention) return null;
25
+ const targetOpen = activeTargetId === undefined || activeTargetId === intervention.targetId;
26
+ return (
27
+ <div
28
+ className={cn(
29
+ "flex shrink-0 items-center gap-2 border-b border-og-status-waiting/25 bg-og-status-waiting/[0.07] px-3 py-2",
30
+ className,
31
+ )}
32
+ role="status"
33
+ >
34
+ <span className="grid size-7 shrink-0 place-items-center rounded-og-sm bg-og-status-waiting/10 text-og-status-waiting">
35
+ <CircleAlertIcon className="size-3.5" />
36
+ </span>
37
+ <span className="min-w-0 flex-1">
38
+ <span className="flex items-center gap-1.5 text-og-control font-medium text-og-fg">
39
+ {interventionTitle(intervention.kind)}
40
+ {interventions.length > 1 ? (
41
+ <span className="rounded-full bg-og-surface-2 px-1.5 py-0.5 text-[10px] font-normal text-og-muted">
42
+ +{interventions.length - 1}
43
+ </span>
44
+ ) : null}
45
+ </span>
46
+ <span className="block truncate text-og-xs text-og-muted">{intervention.reason}</span>
47
+ </span>
48
+ {!targetOpen && onOpen ? (
49
+ <button
50
+ type="button"
51
+ onClick={() => onOpen(intervention)}
52
+ disabled={mutating}
53
+ className="h-7 shrink-0 rounded-og-sm border border-og-border bg-og-surface-1 px-2.5 text-og-control font-medium text-og-fg transition hover:bg-og-surface-2 disabled:opacity-50"
54
+ >
55
+ Open
56
+ </button>
57
+ ) : (
58
+ <button
59
+ type="button"
60
+ onClick={() => onResolve(intervention, "completed")}
61
+ disabled={mutating}
62
+ className="inline-flex h-7 shrink-0 items-center gap-1 rounded-og-sm bg-og-accent px-2.5 text-og-control font-medium text-og-accent-fg transition hover:brightness-105 disabled:opacity-50"
63
+ >
64
+ {mutating ? (
65
+ <LoaderCircleIcon className="size-3 animate-spin" />
66
+ ) : (
67
+ <CheckIcon className="size-3" />
68
+ )}
69
+ Done
70
+ </button>
71
+ )}
72
+ <button
73
+ type="button"
74
+ onClick={() => onResolve(intervention, "dismissed")}
75
+ disabled={mutating}
76
+ className="grid size-7 shrink-0 place-items-center rounded-og-sm text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg disabled:opacity-50"
77
+ aria-label="Cancel request"
78
+ title="Cancel request"
79
+ >
80
+ <XIcon className="size-3.5" />
81
+ </button>
82
+ </div>
83
+ );
84
+ }
85
+
86
+ function interventionTitle(kind: InteractionIntervention["kind"]): string {
87
+ switch (kind) {
88
+ case "manual_login":
89
+ return "Sign in needed";
90
+ case "mfa":
91
+ return "Verification needed";
92
+ case "external_action":
93
+ return "Action needed";
94
+ case "confirmation":
95
+ return "Confirmation needed";
96
+ case "other":
97
+ return "Agent needs your help";
98
+ }
99
+ }
@@ -0,0 +1,2 @@
1
+ export { BrowserViewer } from "./browser-viewer";
2
+ export { ComputerViewer } from "./computer-viewer";