@opengeni/react 0.48.0 → 0.52.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. package/README.md +90 -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 +170 -0
  13. package/dist/artifacts.js.map +1 -0
  14. package/dist/chunk-6BBWWOOU.js +82 -0
  15. package/dist/chunk-6BBWWOOU.js.map +1 -0
  16. package/dist/{chunk-FWXGSNXT.js → chunk-724BW4TW.js} +585 -34
  17. package/dist/chunk-724BW4TW.js.map +1 -0
  18. package/dist/chunk-7I2KB3OK.js +3895 -0
  19. package/dist/chunk-7I2KB3OK.js.map +1 -0
  20. package/dist/chunk-7WCLAEFV.js +2828 -0
  21. package/dist/chunk-7WCLAEFV.js.map +1 -0
  22. package/dist/{chunk-I4MPFDRP.js → chunk-FWS3KC3Z.js} +3 -3
  23. package/dist/{chunk-EKZH6IDG.js → chunk-HDBG7XGO.js} +62 -1
  24. package/dist/chunk-HDBG7XGO.js.map +1 -0
  25. package/dist/chunk-HUIKIDM5.js +2148 -0
  26. package/dist/chunk-HUIKIDM5.js.map +1 -0
  27. package/dist/{chunk-XCNTXWBB.js → chunk-JAGDUCZQ.js} +854 -340
  28. package/dist/chunk-JAGDUCZQ.js.map +1 -0
  29. package/dist/{chunk-LHLYWG5R.js → chunk-JR3QFY6B.js} +413 -37
  30. package/dist/chunk-JR3QFY6B.js.map +1 -0
  31. package/dist/chunk-JVYQRRC4.js +53 -0
  32. package/dist/chunk-JVYQRRC4.js.map +1 -0
  33. package/dist/chunk-KCGXPP5V.js +157 -0
  34. package/dist/chunk-KCGXPP5V.js.map +1 -0
  35. package/dist/chunk-PEI6RANI.js +3234 -0
  36. package/dist/chunk-PEI6RANI.js.map +1 -0
  37. package/dist/{chunk-NJ7M2B6Z.js → chunk-ROIRTM37.js} +18 -10
  38. package/dist/{chunk-NJ7M2B6Z.js.map → chunk-ROIRTM37.js.map} +1 -1
  39. package/dist/{chunk-O4VXOBGF.js → chunk-WSK7G5LE.js} +70 -2
  40. package/dist/chunk-WSK7G5LE.js.map +1 -0
  41. package/dist/{chunk-OL6QVXGF.js → chunk-X4S56Y2H.js} +27 -7
  42. package/dist/{chunk-OL6QVXGF.js.map → chunk-X4S56Y2H.js.map} +1 -1
  43. package/dist/client.d.ts +7 -1
  44. package/dist/clipboard.d.ts +1 -0
  45. package/dist/clipboard.js +7 -0
  46. package/dist/clipboard.js.map +1 -0
  47. package/dist/commands/index.d.ts +3 -3
  48. package/dist/commands/registry.d.ts +1 -1
  49. package/dist/commands/types.d.ts +1 -1
  50. package/dist/components/approval-surface.d.ts +2 -2
  51. package/dist/components/artifacts/artifact-surface.d.ts +18 -0
  52. package/dist/components/artifacts/document-editor.d.ts +172 -0
  53. package/dist/components/artifacts/editable-artifact-ui.d.ts +13 -0
  54. package/dist/components/artifacts/editable-artifact-workbench.d.ts +49 -0
  55. package/dist/components/artifacts/editable-document.d.ts +24 -0
  56. package/dist/components/artifacts/editable-presentation.d.ts +24 -0
  57. package/dist/components/artifacts/editable-spreadsheet.d.ts +32 -0
  58. package/dist/components/artifacts/index.d.ts +8 -0
  59. package/dist/components/artifacts/presentation-editor.d.ts +297 -0
  60. package/dist/components/artifacts/spreadsheet-canvas.d.ts +120 -0
  61. package/dist/components/artifacts/spreadsheet-grid.d.ts +113 -0
  62. package/dist/components/browser-viewer.d.ts +29 -0
  63. package/dist/components/chat-composer.d.ts +8 -8
  64. package/dist/components/code-editor.d.ts +25 -3
  65. package/dist/components/command-palette.d.ts +2 -2
  66. package/dist/components/composer-transcription-control.d.ts +2 -2
  67. package/dist/components/composer.d.ts +17 -17
  68. package/dist/components/computer-viewer.d.ts +23 -0
  69. package/dist/components/copy-button.d.ts +2 -2
  70. package/dist/components/desktop-viewer.d.ts +25 -21
  71. package/dist/components/diff-view.d.ts +1 -1
  72. package/dist/components/enrollment-consent.d.ts +1 -1
  73. package/dist/components/enrollment-device-flow.d.ts +1 -1
  74. package/dist/components/file-browser.d.ts +2 -2
  75. package/dist/components/fleet-tile.d.ts +1 -1
  76. package/dist/components/generated-video-player.d.ts +10 -0
  77. package/dist/components/human-input-form.d.ts +1 -1
  78. package/dist/components/human-input-surface.d.ts +2 -2
  79. package/dist/components/interactive-workbench-viewers.d.ts +2 -0
  80. package/dist/components/machine-card.d.ts +2 -2
  81. package/dist/components/machine-dock-bar.d.ts +3 -3
  82. package/dist/components/machine-health-pill.d.ts +2 -2
  83. package/dist/components/machine-input-display.d.ts +1 -1
  84. package/dist/components/machine-metrics.d.ts +2 -2
  85. package/dist/components/machine-status-pill.d.ts +4 -4
  86. package/dist/components/machines/health.d.ts +1 -1
  87. package/dist/components/machines/machine-detail.d.ts +3 -3
  88. package/dist/components/machines/metric-history-chart.d.ts +1 -1
  89. package/dist/components/machines/metric-sparkline.d.ts +2 -2
  90. package/dist/components/machines/series.d.ts +2 -2
  91. package/dist/components/machines-dashboard.d.ts +2 -2
  92. package/dist/components/markdown.d.ts +1 -1
  93. package/dist/components/message-timeline.d.ts +12 -5
  94. package/dist/components/model-picker.d.ts +2 -2
  95. package/dist/components/model-policy-picker.d.ts +7 -7
  96. package/dist/components/pierre-diff.d.ts +1 -1
  97. package/dist/components/pierre-file.d.ts +1 -1
  98. package/dist/components/queue-draft-policy.d.ts +1 -1
  99. package/dist/components/queue-surface-implementation.d.ts +3 -3
  100. package/dist/components/queue-surface-state.d.ts +4 -4
  101. package/dist/components/queue-surface.d.ts +5 -5
  102. package/dist/components/sandbox-files.d.ts +3 -3
  103. package/dist/components/sandbox-terminal.d.ts +21 -4
  104. package/dist/components/sandbox-workspace.d.ts +25 -9
  105. package/dist/components/session-chrome.d.ts +4 -4
  106. package/dist/components/session-status.d.ts +2 -2
  107. package/dist/components/timeline-annotation-selection.d.ts +9 -0
  108. package/dist/components/timeline-annotations-dialog.d.ts +16 -0
  109. package/dist/components/timeline-annotations.d.ts +13 -0
  110. package/dist/components/tooltip.d.ts +4 -4
  111. package/dist/components/user-message-body.d.ts +2 -2
  112. package/dist/components/workbench-changes.d.ts +2 -2
  113. package/dist/components/workspace-dock.d.ts +1 -1
  114. package/dist/composer.d.ts +11 -11
  115. package/dist/composer.js +5 -5
  116. package/dist/hooks/internal.d.ts +1 -1
  117. package/dist/hooks/use-attached-browsers.d.ts +21 -0
  118. package/dist/hooks/use-available-models.d.ts +2 -2
  119. package/dist/hooks/use-billing-usage.d.ts +1 -1
  120. package/dist/hooks/use-browser-frame-stream.d.ts +28 -0
  121. package/dist/hooks/use-browser-identities.d.ts +26 -0
  122. package/dist/hooks/use-browser-session.d.ts +28 -0
  123. package/dist/hooks/use-browser-sessions.d.ts +32 -0
  124. package/dist/hooks/use-codex-accounts.d.ts +2 -2
  125. package/dist/hooks/use-composer.d.ts +36 -9
  126. package/dist/hooks/use-computer-frame-stream.d.ts +24 -0
  127. package/dist/hooks/use-computer-session.d.ts +28 -0
  128. package/dist/hooks/use-computer-sessions.d.ts +28 -0
  129. package/dist/hooks/use-desktop-stream.d.ts +1 -1
  130. package/dist/hooks/use-environments.d.ts +1 -1
  131. package/dist/hooks/use-file-attachments.d.ts +1 -1
  132. package/dist/hooks/use-goal.d.ts +2 -2
  133. package/dist/hooks/use-human-input.d.ts +2 -2
  134. package/dist/hooks/use-last-started-turn-policy.d.ts +2 -2
  135. package/dist/hooks/use-machine-chip.d.ts +2 -2
  136. package/dist/hooks/use-machines.d.ts +4 -4
  137. package/dist/hooks/use-packs.d.ts +7 -2
  138. package/dist/hooks/use-rigs.d.ts +1 -1
  139. package/dist/hooks/use-sandbox-files.d.ts +6 -4
  140. package/dist/hooks/use-sandbox-git.d.ts +6 -5
  141. package/dist/hooks/use-sandbox-terminal.d.ts +1 -1
  142. package/dist/hooks/use-scheduled-tasks.d.ts +1 -1
  143. package/dist/hooks/use-session-capabilities.d.ts +5 -4
  144. package/dist/hooks/use-session-control.d.ts +1 -1
  145. package/dist/hooks/use-session-events.d.ts +2 -2
  146. package/dist/hooks/use-session-lineage.d.ts +1 -1
  147. package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
  148. package/dist/hooks/use-session.d.ts +2 -2
  149. package/dist/hooks/use-slash-commands.d.ts +1 -1
  150. package/dist/hooks/use-terminal-stream.d.ts +8 -2
  151. package/dist/hooks/use-turn-queue.d.ts +2 -2
  152. package/dist/hooks/use-video-artifact-playback.d.ts +9 -0
  153. package/dist/hooks/use-voice-input.d.ts +1 -1
  154. package/dist/hooks/use-workspace-capture.d.ts +1 -1
  155. package/dist/hooks/use-workspace-edit.d.ts +1 -1
  156. package/dist/hooks/use-workspace-sessions.d.ts +1 -1
  157. package/dist/hooks/use-workspaces.d.ts +1 -1
  158. package/dist/index.d.ts +158 -155
  159. package/dist/index.js +906 -462
  160. package/dist/index.js.map +1 -1
  161. package/dist/interaction.d.ts +23 -0
  162. package/dist/interaction.js +36 -0
  163. package/dist/interaction.js.map +1 -0
  164. package/dist/interactive-workbench-viewers-KHNOHWQE.js +13 -0
  165. package/dist/interactive-workbench-viewers-KHNOHWQE.js.map +1 -0
  166. package/dist/lib/relay-wire.d.ts +4 -0
  167. package/dist/lib/sandbox-liveness.d.ts +9 -0
  168. package/dist/lib/terminal-capability.d.ts +5 -0
  169. package/dist/lib/xterm-theme.d.ts +1 -1
  170. package/dist/machines.d.ts +30 -30
  171. package/dist/machines.js +3 -3
  172. package/dist/provider.d.ts +4 -4
  173. package/dist/{queue-surface-implementation-U5JFG7HT.js → queue-surface-implementation-2KVJTYXH.js} +100 -48
  174. package/dist/queue-surface-implementation-2KVJTYXH.js.map +1 -0
  175. package/dist/realtime/dropdown-menu.d.ts +8 -8
  176. package/dist/realtime/realtime-control.d.ts +6 -6
  177. package/dist/realtime.d.ts +4 -4
  178. package/dist/realtime.js +3 -3
  179. package/dist/session-context.d.ts +19 -1
  180. package/dist/session-ui.d.ts +14 -14
  181. package/dist/session-ui.js +5 -4
  182. package/dist/session.d.ts +28 -28
  183. package/dist/session.js +5 -5
  184. package/dist/timeline/activity-rail.d.ts +5 -3
  185. package/dist/timeline/disclosure-context.d.ts +1 -1
  186. package/dist/timeline/fleet-decision-projection.d.ts +1 -1
  187. package/dist/timeline/fleet-decision-row.d.ts +3 -3
  188. package/dist/timeline/index.d.ts +19 -19
  189. package/dist/timeline/parsers.d.ts +4 -1
  190. package/dist/timeline/projection.d.ts +3 -3
  191. package/dist/timeline/registry.d.ts +8 -2
  192. package/dist/timeline/screenshot-lightbox.d.ts +2 -2
  193. package/dist/timeline/shared.d.ts +21 -10
  194. package/dist/timeline/tool-diff.d.ts +2 -2
  195. package/dist/timeline/tool-renderers.d.ts +1 -1
  196. package/dist/timeline/turn-summary.d.ts +3 -3
  197. package/dist/timeline/types.d.ts +39 -3
  198. package/dist/timeline-annotation-selection-3L7LHGG2.js +187 -0
  199. package/dist/timeline-annotation-selection-3L7LHGG2.js.map +1 -0
  200. package/dist/timeline-annotations-dialog-U7EVLR75.js +196 -0
  201. package/dist/timeline-annotations-dialog-U7EVLR75.js.map +1 -0
  202. package/package.json +36 -7
  203. package/src/artifacts-document.ts +31 -0
  204. package/src/artifacts-presentation.ts +36 -0
  205. package/src/artifacts-spreadsheet.ts +21 -0
  206. package/src/artifacts.ts +86 -0
  207. package/src/client.ts +87 -1
  208. package/src/clipboard.ts +2 -0
  209. package/src/components/artifacts/artifact-surface.tsx +93 -0
  210. package/src/components/artifacts/document-editor.tsx +2255 -0
  211. package/src/components/artifacts/editable-artifact-ui.tsx +89 -0
  212. package/src/components/artifacts/editable-artifact-workbench.tsx +203 -0
  213. package/src/components/artifacts/editable-document.tsx +657 -0
  214. package/src/components/artifacts/editable-presentation.tsx +1020 -0
  215. package/src/components/artifacts/editable-spreadsheet.tsx +733 -0
  216. package/src/components/artifacts/index.ts +99 -0
  217. package/src/components/artifacts/presentation-editor.tsx +3148 -0
  218. package/src/components/artifacts/spreadsheet-canvas.ts +799 -0
  219. package/src/components/artifacts/spreadsheet-grid.tsx +2129 -0
  220. package/src/components/browser-viewer.tsx +1592 -0
  221. package/src/components/chat-composer.tsx +13 -0
  222. package/src/components/code-editor.tsx +89 -30
  223. package/src/components/composer-transcription-control.tsx +10 -6
  224. package/src/components/computer-viewer.tsx +1139 -0
  225. package/src/components/desktop-viewer.tsx +101 -73
  226. package/src/components/generated-video-player.tsx +89 -0
  227. package/src/components/interactive-workbench-viewers.ts +2 -0
  228. package/src/components/machine-input-display.ts +3 -0
  229. package/src/components/message-timeline.tsx +289 -32
  230. package/src/components/queue-surface-implementation.tsx +116 -60
  231. package/src/components/sandbox-files.tsx +2 -2
  232. package/src/components/sandbox-terminal.tsx +106 -41
  233. package/src/components/sandbox-workspace.tsx +215 -87
  234. package/src/components/session-chrome.tsx +53 -13
  235. package/src/components/timeline-annotation-selection.tsx +221 -0
  236. package/src/components/timeline-annotations-dialog.tsx +213 -0
  237. package/src/components/timeline-annotations.tsx +84 -0
  238. package/src/components/workbench-changes.tsx +188 -31
  239. package/src/hooks/use-attached-browsers.ts +152 -0
  240. package/src/hooks/use-browser-frame-stream.ts +316 -0
  241. package/src/hooks/use-browser-identities.ts +225 -0
  242. package/src/hooks/use-browser-session.ts +476 -0
  243. package/src/hooks/use-browser-sessions.ts +281 -0
  244. package/src/hooks/use-composer.ts +769 -24
  245. package/src/hooks/use-computer-frame-stream.ts +311 -0
  246. package/src/hooks/use-computer-session.ts +442 -0
  247. package/src/hooks/use-computer-sessions.ts +245 -0
  248. package/src/hooks/use-machine-chip.ts +5 -3
  249. package/src/hooks/use-machines.ts +33 -3
  250. package/src/hooks/use-packs.ts +51 -0
  251. package/src/hooks/use-sandbox-files.ts +98 -55
  252. package/src/hooks/use-sandbox-git.ts +93 -38
  253. package/src/hooks/use-sandbox-terminal.ts +3 -2
  254. package/src/hooks/use-session-capabilities.ts +59 -46
  255. package/src/hooks/use-terminal-stream.ts +102 -23
  256. package/src/hooks/use-video-artifact-playback.ts +28 -0
  257. package/src/hooks/use-workspace-edit.ts +2 -1
  258. package/src/index.ts +10 -0
  259. package/src/interaction.ts +71 -0
  260. package/src/lib/relay-wire.ts +4 -0
  261. package/src/lib/sandbox-liveness.ts +11 -0
  262. package/src/lib/terminal-capability.ts +10 -0
  263. package/src/session-context.ts +90 -0
  264. package/src/timeline/activity-rail.tsx +19 -3
  265. package/src/timeline/index.ts +3 -0
  266. package/src/timeline/parsers.ts +14 -1
  267. package/src/timeline/projection.ts +211 -14
  268. package/src/timeline/registry.ts +13 -1
  269. package/src/timeline/screenshot-lightbox.tsx +19 -9
  270. package/src/timeline/shared.tsx +56 -8
  271. package/src/timeline/tool-renderers.tsx +303 -50
  272. package/src/timeline/types.ts +55 -3
  273. package/styles/compiled.css +248 -15
  274. package/styles/index.css +15 -0
  275. package/styles/responsive.css +13 -1
  276. package/dist/chunk-EKZH6IDG.js.map +0 -1
  277. package/dist/chunk-FWXGSNXT.js.map +0 -1
  278. package/dist/chunk-LHLYWG5R.js.map +0 -1
  279. package/dist/chunk-O4VXOBGF.js.map +0 -1
  280. package/dist/chunk-XCNTXWBB.js.map +0 -1
  281. package/dist/fleet-decision-projection-SLNZMS42.js +0 -241
  282. package/dist/fleet-decision-projection-SLNZMS42.js.map +0 -1
  283. package/dist/queue-surface-implementation-U5JFG7HT.js.map +0 -1
  284. /package/dist/{chunk-I4MPFDRP.js.map → chunk-FWS3KC3Z.js.map} +0 -0
@@ -0,0 +1,316 @@
1
+ import {
2
+ browserFrameSocketUrl,
3
+ decodeBrowserFrameMessage,
4
+ type BrowserFrame,
5
+ type BrowserFrameStreamOptions,
6
+ type BrowserSessionAttachment,
7
+ } from "@opengeni/sdk/interaction";
8
+ import { useCallback, useEffect, useRef, useState } from "react";
9
+ import {
10
+ type EmbeddedBrowserInteractionClientOverride,
11
+ useEmbeddedBrowserInteraction,
12
+ } from "../session-context";
13
+ import {
14
+ decodeStreamFrame,
15
+ decodeStreamOpenAck,
16
+ encodeStreamOpen,
17
+ STREAM_KIND_BROWSER,
18
+ STREAM_ROLE_CLIENT,
19
+ } from "../lib/relay-wire";
20
+
21
+ export type BrowserFrameConnectionState =
22
+ | "idle"
23
+ | "attaching"
24
+ | "connecting"
25
+ | "live"
26
+ | "reconnecting"
27
+ | "error";
28
+
29
+ export type BrowserFrameWebSocket = Pick<
30
+ WebSocket,
31
+ "binaryType" | "readyState" | "close" | "addEventListener" | "removeEventListener"
32
+ > & { send(data: ArrayBuffer): void };
33
+
34
+ export type BrowserFrameWebSocketFactory = (
35
+ url: string,
36
+ protocols: string[],
37
+ ) => BrowserFrameWebSocket;
38
+
39
+ const RELAY_TAG_OPEN = 1;
40
+ const RELAY_TAG_OPEN_ACK = 2;
41
+ const RELAY_TAG_FRAME = 3;
42
+
43
+ export type UseBrowserFrameStreamOptions = EmbeddedBrowserInteractionClientOverride & {
44
+ browserSessionId: string | null;
45
+ targetId: string | null;
46
+ enabled?: boolean | undefined;
47
+ stream?: BrowserFrameStreamOptions | undefined;
48
+ webSocketFactory?: BrowserFrameWebSocketFactory | undefined;
49
+ };
50
+
51
+ export type UseBrowserFrameStreamResult = {
52
+ state: BrowserFrameConnectionState;
53
+ frame: BrowserFrame | null;
54
+ attachment: Pick<
55
+ BrowserSessionAttachment,
56
+ "browserSessionId" | "controllerGeneration" | "targetId" | "expiresAt"
57
+ > | null;
58
+ error: Error | null;
59
+ reconnect: () => void;
60
+ };
61
+
62
+ /**
63
+ * Reconnecting, latest-only BrowserSession media attachment. Direct placement
64
+ * grants travel as a WebSocket subprotocol; connected-machine grants stay in the
65
+ * relay's first authenticated binary message. Neither transport puts a grant in
66
+ * a URL, event, log, or separately exposed React state.
67
+ */
68
+ export function useBrowserFrameStream(
69
+ options: UseBrowserFrameStreamOptions,
70
+ ): UseBrowserFrameStreamResult {
71
+ const { client, workspaceId } = useEmbeddedBrowserInteraction(options);
72
+ const enabled = options.enabled ?? true;
73
+ const [nonce, setNonce] = useState(0);
74
+ const [result, setResult] = useState<Omit<UseBrowserFrameStreamResult, "reconnect">>({
75
+ state: "idle",
76
+ frame: null,
77
+ attachment: null,
78
+ error: null,
79
+ });
80
+ const latestRef = useRef<{ key: string; sequence: number }>({
81
+ key: "",
82
+ sequence: -1,
83
+ });
84
+ const factoryRef = useRef(options.webSocketFactory);
85
+ const streamRef = useRef(options.stream);
86
+ factoryRef.current = options.webSocketFactory;
87
+ streamRef.current = options.stream;
88
+
89
+ const reconnect = useCallback(() => setNonce((value) => value + 1), []);
90
+
91
+ useEffect(() => {
92
+ const browserSessionId = options.browserSessionId;
93
+ const targetId = options.targetId;
94
+ if (!enabled || !browserSessionId || !targetId || typeof window === "undefined") {
95
+ setResult({ state: "idle", frame: null, attachment: null, error: null });
96
+ return;
97
+ }
98
+
99
+ // Never render or target input against a frame from the previously selected
100
+ // resource while its replacement attachment is negotiating.
101
+ latestRef.current = { key: "", sequence: -1 };
102
+ setResult({
103
+ state: "attaching",
104
+ frame: null,
105
+ attachment: null,
106
+ error: null,
107
+ });
108
+
109
+ let disposed = false;
110
+ let socket: BrowserFrameWebSocket | null = null;
111
+ let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
112
+ let expiryTimer: ReturnType<typeof setTimeout> | null = null;
113
+ let failures = 0;
114
+ let controllerGeneration: string | null = null;
115
+ let activeStream: BrowserSessionAttachment["stream"] | null = null;
116
+ let relayAccepted = false;
117
+ const attachmentAbort = new AbortController();
118
+
119
+ const clearSocket = () => {
120
+ if (!socket) return;
121
+ socket.removeEventListener("open", onOpen);
122
+ socket.removeEventListener("message", onMessage);
123
+ socket.removeEventListener("error", onError);
124
+ socket.removeEventListener("close", onClose);
125
+ if (socket.readyState === 0 || socket.readyState === 1) {
126
+ socket.close(1000, "viewer detached");
127
+ }
128
+ socket = null;
129
+ };
130
+
131
+ const scheduleReconnect = () => {
132
+ if (disposed || reconnectTimer) return;
133
+ const delay = Math.min(5_000, 250 * 2 ** Math.min(failures, 5));
134
+ failures += 1;
135
+ setResult((current) => ({ ...current, state: "reconnecting" }));
136
+ reconnectTimer = setTimeout(() => {
137
+ reconnectTimer = null;
138
+ void connect();
139
+ }, delay);
140
+ };
141
+
142
+ const fail = (cause: unknown) => {
143
+ if (disposed) return;
144
+ const error = cause instanceof Error ? cause : new Error(String(cause));
145
+ setResult((current) => ({ ...current, state: "error", error }));
146
+ clearSocket();
147
+ scheduleReconnect();
148
+ };
149
+
150
+ const onOpen = () => {
151
+ if (disposed) return;
152
+ failures = 0;
153
+ if (activeStream?.kind === "relay") {
154
+ const body = encodeStreamOpen({
155
+ channel: {
156
+ ...activeStream.channel,
157
+ kind: STREAM_KIND_BROWSER,
158
+ },
159
+ token: activeStream.token,
160
+ role: STREAM_ROLE_CLIENT,
161
+ resumeFromSeq: "0",
162
+ });
163
+ socket?.send(relayDatagram(RELAY_TAG_OPEN, body));
164
+ return;
165
+ }
166
+ setResult((current) => ({ ...current, state: "live", error: null }));
167
+ };
168
+
169
+ const onMessage = (event: MessageEvent) => {
170
+ void (async () => {
171
+ try {
172
+ const bytes = await messageBytes(event.data);
173
+ if (disposed) return;
174
+ let frameBytes = bytes;
175
+ if (activeStream?.kind === "relay") {
176
+ if (bytes.length < 1) throw new Error("browser relay returned an empty message");
177
+ const tag = bytes[0];
178
+ const body = bytes.subarray(1);
179
+ if (tag === RELAY_TAG_OPEN_ACK) {
180
+ const ack = decodeStreamOpenAck(body);
181
+ if (!ack.accepted) {
182
+ throw new Error(ack.error?.message ?? "browser stream was rejected by the relay");
183
+ }
184
+ relayAccepted = true;
185
+ return;
186
+ }
187
+ if (tag !== RELAY_TAG_FRAME || !relayAccepted) return;
188
+ frameBytes = decodeStreamFrame(body).data;
189
+ }
190
+ const frame = decodeBrowserFrameMessage(frameBytes);
191
+ if (frame.browserSessionId !== browserSessionId || frame.targetId !== targetId) {
192
+ throw new Error("browser frame belongs to another resource");
193
+ }
194
+ if (!controllerGeneration || frame.controllerGeneration !== controllerGeneration) {
195
+ throw new Error("browser frame belongs to a stale controller");
196
+ }
197
+ const key = `${browserSessionId}:${targetId}:${frame.controllerGeneration}:${frame.targetGeneration}:${frame.documentGeneration}`;
198
+ if (latestRef.current.key === key && frame.sequence <= latestRef.current.sequence) return;
199
+ latestRef.current = { key, sequence: frame.sequence };
200
+ setResult((current) => ({
201
+ ...current,
202
+ state: "live",
203
+ frame,
204
+ error: null,
205
+ }));
206
+ } catch (cause) {
207
+ fail(cause);
208
+ }
209
+ })();
210
+ };
211
+
212
+ const onError = () => fail(new Error("Browser view lost connection."));
213
+
214
+ const onClose = () => {
215
+ if (disposed) return;
216
+ clearSocket();
217
+ scheduleReconnect();
218
+ };
219
+
220
+ const connect = async () => {
221
+ if (disposed) return;
222
+ clearSocket();
223
+ if (expiryTimer) clearTimeout(expiryTimer);
224
+ setResult((current) => ({
225
+ ...current,
226
+ state: current.attachment ? "reconnecting" : "attaching",
227
+ error: null,
228
+ }));
229
+ try {
230
+ const attachment = await client.attachBrowserSession(
231
+ workspaceId,
232
+ browserSessionId,
233
+ {
234
+ targetId,
235
+ expiresInSeconds: 120,
236
+ ...(streamRef.current ? { stream: streamRef.current } : {}),
237
+ },
238
+ { signal: attachmentAbort.signal },
239
+ );
240
+ if (disposed) return;
241
+ if (attachment.browserSessionId !== browserSessionId || attachment.targetId !== targetId) {
242
+ throw new Error("browser attachment does not match the requested resource");
243
+ }
244
+ controllerGeneration = attachment.controllerGeneration;
245
+ activeStream = attachment.stream;
246
+ relayAccepted = false;
247
+ setResult((current) => ({
248
+ ...current,
249
+ state: "connecting",
250
+ attachment: {
251
+ browserSessionId: attachment.browserSessionId,
252
+ controllerGeneration: attachment.controllerGeneration,
253
+ targetId: attachment.targetId,
254
+ expiresAt: attachment.expiresAt,
255
+ },
256
+ error: null,
257
+ }));
258
+ const createSocket =
259
+ factoryRef.current ??
260
+ ((url: string, protocols: string[]) => new WebSocket(url, protocols));
261
+ socket = createSocket(
262
+ attachment.stream.kind === "relay"
263
+ ? attachment.stream.url
264
+ : browserFrameSocketUrl(attachment, streamRef.current),
265
+ attachment.stream.kind === "direct_websocket" ? [...attachment.stream.protocols] : [],
266
+ );
267
+ socket.binaryType = "arraybuffer";
268
+ socket.addEventListener("open", onOpen);
269
+ socket.addEventListener("message", onMessage);
270
+ socket.addEventListener("error", onError);
271
+ socket.addEventListener("close", onClose);
272
+ const expiresAt = Date.parse(attachment.expiresAt);
273
+ const refreshIn = Number.isFinite(expiresAt)
274
+ ? Math.max(1_000, expiresAt - Date.now() - 5_000)
275
+ : 60_000;
276
+ expiryTimer = setTimeout(() => {
277
+ if (disposed) return;
278
+ clearSocket();
279
+ void connect();
280
+ }, refreshIn);
281
+ } catch (cause) {
282
+ if (attachmentAbort.signal.aborted || disposed) return;
283
+ fail(cause);
284
+ }
285
+ };
286
+
287
+ void connect();
288
+ return () => {
289
+ disposed = true;
290
+ attachmentAbort.abort();
291
+ if (reconnectTimer) clearTimeout(reconnectTimer);
292
+ if (expiryTimer) clearTimeout(expiryTimer);
293
+ clearSocket();
294
+ };
295
+ }, [client, enabled, nonce, options.browserSessionId, options.targetId, workspaceId]);
296
+
297
+ return { ...result, reconnect };
298
+ }
299
+
300
+ function relayDatagram(tag: number, body: Uint8Array): ArrayBuffer {
301
+ const message = new Uint8Array(body.length + 1);
302
+ message[0] = tag;
303
+ message.set(body, 1);
304
+ return message.buffer as ArrayBuffer;
305
+ }
306
+
307
+ async function messageBytes(value: unknown): Promise<Uint8Array> {
308
+ if (value instanceof ArrayBuffer) return new Uint8Array(value);
309
+ if (ArrayBuffer.isView(value)) {
310
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength).slice();
311
+ }
312
+ if (typeof Blob !== "undefined" && value instanceof Blob) {
313
+ return new Uint8Array(await value.arrayBuffer());
314
+ }
315
+ throw new Error("browser frame stream returned a non-binary message");
316
+ }
@@ -0,0 +1,225 @@
1
+ import type {
2
+ BrowserIdentity,
3
+ BrowserIdentityMutationResponse,
4
+ BrowserRevisionListResponse,
5
+ CreateBrowserIdentityRequest,
6
+ PublishBrowserRevisionRequest,
7
+ PublishBrowserRevisionResponse,
8
+ } from "@opengeni/sdk/interaction";
9
+ import { useCallback, useEffect, useRef, useState } from "react";
10
+ import type { EmbeddedBrowserInteractionClientLike } from "../client";
11
+ import {
12
+ type EmbeddedBrowserInteractionClientOverride,
13
+ useEmbeddedBrowserInteraction,
14
+ } from "../session-context";
15
+
16
+ export type UseBrowserIdentitiesOptions = EmbeddedBrowserInteractionClientOverride & {
17
+ enabled?: boolean | undefined;
18
+ };
19
+
20
+ export type UseBrowserIdentitiesResult = {
21
+ revision: number;
22
+ identities: BrowserIdentity[];
23
+ loading: boolean;
24
+ refreshing: boolean;
25
+ mutating: boolean;
26
+ error: Error | null;
27
+ refresh: () => Promise<void>;
28
+ create: (
29
+ request: Omit<CreateBrowserIdentityRequest, "operationId"> & {
30
+ operationId?: string;
31
+ },
32
+ ) => Promise<BrowserIdentityMutationResponse>;
33
+ revisions: (identityId: string) => Promise<BrowserRevisionListResponse>;
34
+ publish: (
35
+ browserSessionId: string,
36
+ request: Omit<PublishBrowserRevisionRequest, "operationId"> & {
37
+ operationId?: string;
38
+ },
39
+ ) => Promise<PublishBrowserRevisionResponse>;
40
+ };
41
+
42
+ /** Workspace browser-profile library and explicit immutable-version mutations. */
43
+ export function useBrowserIdentities(
44
+ options: UseBrowserIdentitiesOptions = {},
45
+ ): UseBrowserIdentitiesResult {
46
+ const { client, workspaceId } = useEmbeddedBrowserInteraction(options);
47
+ const enabled = options.enabled ?? true;
48
+ const [state, setState] = useState(() => emptyState(workspaceId, enabled));
49
+ const [mutationCount, setMutationCount] = useState(0);
50
+ const requestRef = useRef<{ id: number; controller: AbortController | null }>({
51
+ id: 0,
52
+ controller: null,
53
+ });
54
+ const mountedRef = useRef(true);
55
+ const visibleState = state.workspaceId === workspaceId ? state : emptyState(workspaceId, enabled);
56
+
57
+ const cancelLoad = useCallback(() => {
58
+ const id = requestRef.current.id + 1;
59
+ requestRef.current.controller?.abort();
60
+ requestRef.current = { id, controller: null };
61
+ }, []);
62
+
63
+ const load = useCallback(
64
+ async (foreground: boolean): Promise<void> => {
65
+ if (!enabled) return;
66
+ const id = requestRef.current.id + 1;
67
+ requestRef.current.controller?.abort();
68
+ const controller = new AbortController();
69
+ requestRef.current = { id, controller };
70
+ setState((current) => ({
71
+ ...(current.workspaceId === workspaceId ? current : emptyState(workspaceId, true)),
72
+ loading: foreground,
73
+ refreshing: !foreground,
74
+ error: foreground || current.workspaceId !== workspaceId ? null : current.error,
75
+ }));
76
+ try {
77
+ const response = await client.listBrowserIdentities(workspaceId, {
78
+ signal: controller.signal,
79
+ });
80
+ if (!mountedRef.current || requestRef.current.id !== id) return;
81
+ setState({
82
+ workspaceId,
83
+ revision: response.revision,
84
+ identities: sortIdentities(response.identities),
85
+ loading: false,
86
+ refreshing: false,
87
+ error: null,
88
+ });
89
+ } catch (cause) {
90
+ if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id)
91
+ return;
92
+ setState((current) => ({
93
+ ...(current.workspaceId === workspaceId ? current : emptyState(workspaceId, true)),
94
+ loading: false,
95
+ refreshing: false,
96
+ error: cause instanceof Error ? cause : new Error(String(cause)),
97
+ }));
98
+ } finally {
99
+ if (requestRef.current.id === id) requestRef.current = { id, controller: null };
100
+ }
101
+ },
102
+ [client, enabled, workspaceId],
103
+ );
104
+
105
+ const refresh = useCallback(async () => await load(false), [load]);
106
+
107
+ useEffect(() => {
108
+ mountedRef.current = true;
109
+ if (!enabled) {
110
+ cancelLoad();
111
+ setState(emptyState(workspaceId, false));
112
+ return;
113
+ }
114
+ void load(true);
115
+ return () => {
116
+ mountedRef.current = false;
117
+ cancelLoad();
118
+ };
119
+ }, [cancelLoad, enabled, load, workspaceId]);
120
+
121
+ const mutate = useCallback(async <T>(operation: () => Promise<T>): Promise<T> => {
122
+ setMutationCount((count) => count + 1);
123
+ try {
124
+ return await operation();
125
+ } finally {
126
+ if (mountedRef.current) setMutationCount((count) => Math.max(0, count - 1));
127
+ }
128
+ }, []);
129
+
130
+ const create = useCallback(
131
+ async (
132
+ request: Omit<CreateBrowserIdentityRequest, "operationId"> & {
133
+ operationId?: string;
134
+ },
135
+ ): Promise<BrowserIdentityMutationResponse> =>
136
+ await mutate(async () => {
137
+ const response = await client.createBrowserIdentity(workspaceId, {
138
+ ...request,
139
+ operationId: request.operationId ?? crypto.randomUUID(),
140
+ });
141
+ setState((current) => mergeIdentity(current, workspaceId, response.identity));
142
+ return response;
143
+ }),
144
+ [client, mutate, workspaceId],
145
+ );
146
+
147
+ const revisions = useCallback(
148
+ async (identityId: string): Promise<BrowserRevisionListResponse> =>
149
+ await client.listBrowserRevisions(workspaceId, identityId),
150
+ [client, workspaceId],
151
+ );
152
+
153
+ const publish = useCallback(
154
+ async (
155
+ browserSessionId: string,
156
+ request: Omit<PublishBrowserRevisionRequest, "operationId"> & {
157
+ operationId?: string;
158
+ },
159
+ ): Promise<PublishBrowserRevisionResponse> =>
160
+ await mutate(async () => {
161
+ const response = await client.publishBrowserRevision(workspaceId, browserSessionId, {
162
+ ...request,
163
+ operationId: request.operationId ?? crypto.randomUUID(),
164
+ });
165
+ setState((current) => mergeIdentity(current, workspaceId, response.identity));
166
+ return response;
167
+ }),
168
+ [client, mutate, workspaceId],
169
+ );
170
+
171
+ return {
172
+ revision: visibleState.revision,
173
+ identities: visibleState.identities,
174
+ loading: visibleState.loading,
175
+ refreshing: visibleState.refreshing,
176
+ mutating: mutationCount > 0,
177
+ error: visibleState.error,
178
+ refresh,
179
+ create,
180
+ revisions,
181
+ publish,
182
+ };
183
+ }
184
+
185
+ function emptyState(workspaceId: string, loading: boolean) {
186
+ return {
187
+ workspaceId,
188
+ revision: 0,
189
+ identities: [] as BrowserIdentity[],
190
+ loading,
191
+ refreshing: false,
192
+ error: null as Error | null,
193
+ };
194
+ }
195
+
196
+ function sortIdentities(identities: readonly BrowserIdentity[]): BrowserIdentity[] {
197
+ return [...identities].sort(
198
+ (left, right) =>
199
+ timestamp(right.updatedAt) - timestamp(left.updatedAt) || left.name.localeCompare(right.name),
200
+ );
201
+ }
202
+
203
+ function timestamp(value: string): number {
204
+ const parsed = Date.parse(value);
205
+ return Number.isFinite(parsed) ? parsed : 0;
206
+ }
207
+
208
+ function mergeIdentity(
209
+ current: ReturnType<typeof emptyState>,
210
+ workspaceId: string,
211
+ identity: BrowserIdentity,
212
+ ): ReturnType<typeof emptyState> {
213
+ if (current.workspaceId !== workspaceId) return current;
214
+ return {
215
+ ...current,
216
+ identities: sortIdentities([
217
+ ...current.identities.filter((candidate) => candidate.id !== identity.id),
218
+ identity,
219
+ ]),
220
+ error: null,
221
+ };
222
+ }
223
+
224
+ /** Exported for narrow proxy implementations and tests. */
225
+ export type BrowserIdentitiesClient = EmbeddedBrowserInteractionClientLike;