@opengeni/react 1.0.1 → 2.3.0-canary.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 (174) hide show
  1. package/README.md +33 -10
  2. package/dist/artifacts-spreadsheet.js +1 -1
  3. package/dist/artifacts.js +1 -1
  4. package/dist/browser-viewer-GXUICIAG.js +14 -0
  5. package/dist/{chunk-R3AZOAB5.js → chunk-4MDLEHXL.js} +9 -115
  6. package/dist/chunk-4MDLEHXL.js.map +1 -0
  7. package/dist/chunk-5YK2ZDIG.js +116 -0
  8. package/dist/chunk-5YK2ZDIG.js.map +1 -0
  9. package/dist/chunk-H5YGYNXS.js +329 -0
  10. package/dist/chunk-H5YGYNXS.js.map +1 -0
  11. package/dist/{chunk-7WCLAEFV.js → chunk-ICA6R6TW.js} +2 -2
  12. package/dist/{chunk-7WCLAEFV.js.map → chunk-ICA6R6TW.js.map} +1 -1
  13. package/dist/{chunk-IGC6A2ZI.js → chunk-JOAVA2TW.js} +589 -217
  14. package/dist/chunk-JOAVA2TW.js.map +1 -0
  15. package/dist/{chunk-Y7OEDUJ5.js → chunk-LS4POUTY.js} +1057 -426
  16. package/dist/chunk-LS4POUTY.js.map +1 -0
  17. package/dist/{chunk-5UQPNLIO.js → chunk-LTCHJ4MS.js} +9 -9
  18. package/dist/chunk-LTCHJ4MS.js.map +1 -0
  19. package/dist/{chunk-C5ZPSSS5.js → chunk-NEXLLTP4.js} +2 -1
  20. package/dist/{chunk-C5ZPSSS5.js.map → chunk-NEXLLTP4.js.map} +1 -1
  21. package/dist/chunk-QGBJ6GHG.js +2300 -0
  22. package/dist/chunk-QGBJ6GHG.js.map +1 -0
  23. package/dist/{chunk-6KLX2RH5.js → chunk-RSXJN73N.js} +365 -21
  24. package/dist/chunk-RSXJN73N.js.map +1 -0
  25. package/dist/{chunk-B6WPZSTT.js → chunk-SE4W4WQ3.js} +78 -16
  26. package/dist/chunk-SE4W4WQ3.js.map +1 -0
  27. package/dist/{chunk-MKPIAGVL.js → chunk-W6NFJXSA.js} +8 -19
  28. package/dist/chunk-W6NFJXSA.js.map +1 -0
  29. package/dist/{chunk-XBZWOM2K.js → chunk-X4A66EWZ.js} +8 -3
  30. package/dist/chunk-X4A66EWZ.js.map +1 -0
  31. package/dist/chunk-X4JAFD5R.js +3907 -0
  32. package/dist/chunk-X4JAFD5R.js.map +1 -0
  33. package/dist/{chunk-HA46CYQK.js → chunk-XXWH2JLC.js} +792 -206
  34. package/dist/chunk-XXWH2JLC.js.map +1 -0
  35. package/dist/client.d.ts +2 -2
  36. package/dist/components/browser-viewer.d.ts +7 -1
  37. package/dist/components/computer-viewer.d.ts +4 -1
  38. package/dist/components/enrollment-consent.d.ts +6 -3
  39. package/dist/components/file-browser.d.ts +7 -1
  40. package/dist/components/human-input-form.d.ts +1 -1
  41. package/dist/components/human-input-surface.d.ts +1 -1
  42. package/dist/components/machine-card.d.ts +4 -1
  43. package/dist/components/machines/machine-detail.d.ts +4 -1
  44. package/dist/components/machines/machine-operation-policy.d.ts +11 -0
  45. package/dist/components/machines-dashboard.d.ts +3 -1
  46. package/dist/components/markdown.d.ts +20 -1
  47. package/dist/components/message-timeline.d.ts +1 -1
  48. package/dist/components/model-policy-picker.d.ts +2 -0
  49. package/dist/components/pierre-file.d.ts +1 -1
  50. package/dist/components/sandbox-files.d.ts +8 -2
  51. package/dist/components/sandbox-workspace.d.ts +33 -3
  52. package/dist/components/session-chrome.d.ts +15 -3
  53. package/dist/components/workspace-dock.d.ts +6 -2
  54. package/dist/composer.js +5 -5
  55. package/dist/computer-viewer-ZOP4Q7A7.js +16 -0
  56. package/dist/computer-viewer-ZOP4Q7A7.js.map +1 -0
  57. package/dist/hooks/channel-cache.d.ts +23 -0
  58. package/dist/hooks/shared-machines-list.d.ts +14 -0
  59. package/dist/hooks/use-attached-browsers.d.ts +2 -1
  60. package/dist/hooks/use-channels.d.ts +1 -0
  61. package/dist/hooks/use-composer.d.ts +37 -9
  62. package/dist/hooks/use-machine-chip.d.ts +3 -3
  63. package/dist/hooks/use-machines.d.ts +19 -1
  64. package/dist/hooks/use-sandbox-files.d.ts +1 -0
  65. package/dist/hooks/use-session-background-commands.d.ts +14 -0
  66. package/dist/hooks/use-turn-queue.d.ts +12 -0
  67. package/dist/hooks/use-workspace-sessions.d.ts +1 -0
  68. package/dist/index.d.ts +7 -5
  69. package/dist/index.js +1096 -623
  70. package/dist/index.js.map +1 -1
  71. package/dist/interaction.js +29 -181
  72. package/dist/interaction.js.map +1 -1
  73. package/dist/lib/interaction-errors.d.ts +3 -0
  74. package/dist/machines.d.ts +4 -2
  75. package/dist/machines.js +10 -2
  76. package/dist/model-policy.d.ts +1 -1
  77. package/dist/model-policy.js +1 -1
  78. package/dist/platform-activity-row-BJHUHXAT.js +169 -0
  79. package/dist/platform-activity-row-BJHUHXAT.js.map +1 -0
  80. package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-BSKUWHRI.js} +76 -16
  81. package/dist/queue-surface-implementation-BSKUWHRI.js.map +1 -0
  82. package/dist/realtime/realtime-control.d.ts +6 -3
  83. package/dist/realtime.js +36 -14
  84. package/dist/realtime.js.map +1 -1
  85. package/dist/session-ui.d.ts +1 -1
  86. package/dist/session-ui.js +6 -2
  87. package/dist/session.d.ts +2 -2
  88. package/dist/session.js +4 -4
  89. package/dist/timeline/index.d.ts +1 -1
  90. package/dist/timeline/platform-activity-row.d.ts +15 -0
  91. package/dist/timeline/screenshot-lightbox.d.ts +1 -1
  92. package/dist/timeline/shared.d.ts +4 -2
  93. package/dist/timeline/types.d.ts +50 -5
  94. package/dist/types/machines.d.ts +2 -2
  95. package/package.json +3 -3
  96. package/src/client.ts +12 -1
  97. package/src/components/artifacts/editable-spreadsheet.tsx +1 -1
  98. package/src/components/browser-viewer.tsx +361 -58
  99. package/src/components/chat-composer.tsx +1 -1
  100. package/src/components/composer.tsx +6 -6
  101. package/src/components/computer-viewer.tsx +368 -115
  102. package/src/components/enrollment-consent.tsx +64 -6
  103. package/src/components/file-browser.tsx +117 -2
  104. package/src/components/human-input-form.tsx +103 -59
  105. package/src/components/human-input-surface.tsx +1 -1
  106. package/src/components/machine-card.tsx +111 -0
  107. package/src/components/machine-input-display.ts +15 -0
  108. package/src/components/machines/machine-detail.tsx +47 -0
  109. package/src/components/machines/machine-operation-policy.tsx +179 -0
  110. package/src/components/machines-dashboard.tsx +6 -0
  111. package/src/components/markdown.tsx +174 -20
  112. package/src/components/message-timeline.tsx +215 -18
  113. package/src/components/model-policy-picker.tsx +9 -17
  114. package/src/components/pierre-file.tsx +1 -1
  115. package/src/components/queue-surface-implementation.tsx +82 -16
  116. package/src/components/sandbox-files.tsx +135 -19
  117. package/src/components/sandbox-workspace.tsx +204 -92
  118. package/src/components/session-chrome.tsx +415 -208
  119. package/src/components/session-status.tsx +1 -1
  120. package/src/components/user-message-body.tsx +168 -48
  121. package/src/components/workspace-dock.tsx +141 -175
  122. package/src/hooks/channel-cache.ts +104 -0
  123. package/src/hooks/shared-machines-list.ts +239 -0
  124. package/src/hooks/use-attached-browsers.ts +5 -1
  125. package/src/hooks/use-browser-frame-stream.ts +6 -1
  126. package/src/hooks/use-browser-session.ts +6 -4
  127. package/src/hooks/use-browser-sessions.ts +7 -1
  128. package/src/hooks/use-channels.ts +32 -10
  129. package/src/hooks/use-composer.ts +760 -291
  130. package/src/hooks/use-computer-frame-stream.ts +14 -9
  131. package/src/hooks/use-computer-session.ts +16 -14
  132. package/src/hooks/use-computer-sessions.ts +8 -1
  133. package/src/hooks/use-last-started-turn-policy.ts +1 -1
  134. package/src/hooks/use-machine-chip.ts +23 -6
  135. package/src/hooks/use-machines.ts +104 -6
  136. package/src/hooks/use-sandbox-files.ts +195 -140
  137. package/src/hooks/use-sandbox-git.ts +58 -29
  138. package/src/hooks/use-session-background-commands.ts +56 -0
  139. package/src/hooks/use-session-capabilities.ts +18 -0
  140. package/src/hooks/use-turn-queue.ts +111 -12
  141. package/src/hooks/use-workspace-sessions.ts +5 -1
  142. package/src/index.ts +26 -4
  143. package/src/lib/interaction-errors.ts +18 -0
  144. package/src/machines.ts +9 -1
  145. package/src/model-policy.ts +10 -2
  146. package/src/realtime/realtime-control.tsx +53 -17
  147. package/src/session-context.ts +1 -0
  148. package/src/session-ui.ts +6 -1
  149. package/src/session.ts +9 -1
  150. package/src/timeline/activity-rail.tsx +20 -75
  151. package/src/timeline/index.ts +4 -0
  152. package/src/timeline/platform-activity-row.tsx +201 -0
  153. package/src/timeline/projection.ts +529 -25
  154. package/src/timeline/screenshot-lightbox.tsx +48 -14
  155. package/src/timeline/shared.tsx +8 -2
  156. package/src/timeline/tool-renderers.tsx +104 -6
  157. package/src/timeline/turn-summary.tsx +6 -1
  158. package/src/timeline/types.ts +77 -2
  159. package/src/types/machines.ts +10 -0
  160. package/styles/compiled.css +115 -104
  161. package/dist/chunk-5UQPNLIO.js.map +0 -1
  162. package/dist/chunk-6KLX2RH5.js.map +0 -1
  163. package/dist/chunk-B6WPZSTT.js.map +0 -1
  164. package/dist/chunk-HA46CYQK.js.map +0 -1
  165. package/dist/chunk-IGC6A2ZI.js.map +0 -1
  166. package/dist/chunk-LLOLUHVG.js +0 -5937
  167. package/dist/chunk-LLOLUHVG.js.map +0 -1
  168. package/dist/chunk-MKPIAGVL.js.map +0 -1
  169. package/dist/chunk-R3AZOAB5.js.map +0 -1
  170. package/dist/chunk-XBZWOM2K.js.map +0 -1
  171. package/dist/chunk-Y7OEDUJ5.js.map +0 -1
  172. package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
  173. package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
  174. /package/dist/{interactive-workbench-viewers-TOHBXJEE.js.map → browser-viewer-GXUICIAG.js.map} +0 -0
@@ -0,0 +1,3907 @@
1
+ import {
2
+ InteractionInterventionBanner,
3
+ isNonRetryableInteractionError,
4
+ isSourcePlacementChangedError,
5
+ useInteractionInterventions,
6
+ useInteractionInvalidation
7
+ } from "./chunk-H5YGYNXS.js";
8
+ import {
9
+ STREAM_CLOSE_REASON_NORMAL,
10
+ STREAM_KIND_BROWSER,
11
+ STREAM_ROLE_CLIENT,
12
+ decodeStreamFrame,
13
+ decodeStreamOpenAck,
14
+ encodeStreamClose,
15
+ encodeStreamOpen,
16
+ interactionHostPlatform
17
+ } from "./chunk-5YK2ZDIG.js";
18
+ import {
19
+ usePageLiveActivity
20
+ } from "./chunk-KZ73RL76.js";
21
+ import {
22
+ useEmbeddedBrowserInteraction
23
+ } from "./chunk-NEXLLTP4.js";
24
+ import {
25
+ formatBytes
26
+ } from "./chunk-LAKLF4PA.js";
27
+ import {
28
+ cn
29
+ } from "./chunk-22KP6LR5.js";
30
+ import {
31
+ copyTextToClipboard
32
+ } from "./chunk-JVYQRRC4.js";
33
+
34
+ // src/components/browser-viewer.tsx
35
+ import { interactionControlFailureFromError } from "@opengeni/sdk/interaction";
36
+ import {
37
+ BugIcon,
38
+ ArchiveIcon,
39
+ CheckIcon,
40
+ ChevronDownIcon,
41
+ CircleAlertIcon,
42
+ DownloadIcon,
43
+ FileCheck2Icon,
44
+ Globe2Icon,
45
+ LoaderCircleIcon,
46
+ MonitorIcon,
47
+ PlusIcon,
48
+ RefreshCwIcon,
49
+ RotateCcwIcon,
50
+ SaveIcon,
51
+ UserRoundIcon,
52
+ XIcon,
53
+ ZapIcon
54
+ } from "lucide-react";
55
+ import {
56
+ useCallback as useCallback8,
57
+ useEffect as useEffect8,
58
+ useId,
59
+ useMemo as useMemo2,
60
+ useRef as useRef8,
61
+ useState as useState8
62
+ } from "react";
63
+
64
+ // src/hooks/use-browser-frame-stream.ts
65
+ import {
66
+ browserFrameSocketUrl,
67
+ decodeBrowserFrameMessage
68
+ } from "@opengeni/sdk/interaction";
69
+ import { useCallback, useEffect, useRef, useState } from "react";
70
+ var RELAY_TAG_OPEN = 1;
71
+ var RELAY_TAG_OPEN_ACK = 2;
72
+ var RELAY_TAG_FRAME = 3;
73
+ var RELAY_TAG_CLOSE = 4;
74
+ var MAX_AUTOMATIC_RECONNECTS_WITHOUT_A_FRAME = 2;
75
+ function useBrowserFrameStream(options) {
76
+ const { client, workspaceId } = useEmbeddedBrowserInteraction(options);
77
+ const enabled = options.enabled ?? true;
78
+ const [nonce, setNonce] = useState(0);
79
+ const [result, setResult] = useState({
80
+ state: "idle",
81
+ frame: null,
82
+ attachment: null,
83
+ error: null
84
+ });
85
+ const latestRef = useRef({
86
+ key: "",
87
+ sequence: -1
88
+ });
89
+ const factoryRef = useRef(options.webSocketFactory);
90
+ const streamRef = useRef(options.stream);
91
+ factoryRef.current = options.webSocketFactory;
92
+ streamRef.current = options.stream;
93
+ const reconnect = useCallback(() => setNonce((value) => value + 1), []);
94
+ useEffect(() => {
95
+ const browserSessionId = options.browserSessionId;
96
+ const targetId = options.targetId;
97
+ if (!enabled || !browserSessionId || !targetId || typeof window === "undefined") {
98
+ setResult({ state: "idle", frame: null, attachment: null, error: null });
99
+ return;
100
+ }
101
+ latestRef.current = { key: "", sequence: -1 };
102
+ setResult({
103
+ state: "attaching",
104
+ frame: null,
105
+ attachment: null,
106
+ error: null
107
+ });
108
+ let disposed = false;
109
+ let socket = null;
110
+ let socketHandlers = null;
111
+ let reconnectTimer = null;
112
+ let expiryTimer = null;
113
+ let failures = 0;
114
+ let controllerGeneration = null;
115
+ let activeAttachment = null;
116
+ let activeStream = null;
117
+ let attachmentExpiresAt = 0;
118
+ let lastRelaySequence = null;
119
+ let relayAccepted = false;
120
+ let pendingFrame = null;
121
+ let decodingFrame = false;
122
+ const attachmentAbort = new AbortController();
123
+ const clearSocket = (terminateProducer = false) => {
124
+ if (!socket) return;
125
+ const current = socket;
126
+ const handlers = socketHandlers;
127
+ if (terminateProducer && activeStream?.kind === "relay" && socket.readyState === 1) {
128
+ try {
129
+ socket.send(
130
+ relayDatagram(
131
+ RELAY_TAG_CLOSE,
132
+ encodeStreamClose({
133
+ channelId: activeStream.channel.channelId,
134
+ reason: STREAM_CLOSE_REASON_NORMAL,
135
+ message: "viewer detached"
136
+ })
137
+ )
138
+ );
139
+ } catch {
140
+ }
141
+ }
142
+ if (handlers) {
143
+ current.removeEventListener("open", handlers.open);
144
+ current.removeEventListener("message", handlers.message);
145
+ current.removeEventListener("error", handlers.error);
146
+ current.removeEventListener("close", handlers.close);
147
+ }
148
+ if (current.readyState === 0 || current.readyState === 1) {
149
+ current.close(1e3, "viewer detached");
150
+ }
151
+ socket = null;
152
+ socketHandlers = null;
153
+ pendingFrame = null;
154
+ };
155
+ const scheduleReconnect = () => {
156
+ if (disposed || reconnectTimer) return;
157
+ if (failures >= MAX_AUTOMATIC_RECONNECTS_WITHOUT_A_FRAME) return;
158
+ const delay = Math.min(5e3, 250 * 2 ** Math.min(failures, 5));
159
+ failures += 1;
160
+ setResult((current) => ({ ...current, state: "reconnecting" }));
161
+ reconnectTimer = setTimeout(() => {
162
+ reconnectTimer = null;
163
+ connectSocket();
164
+ }, delay);
165
+ };
166
+ const fail = (cause, reconnectAutomatically = true) => {
167
+ if (disposed) return;
168
+ const error = cause instanceof Error ? cause : new Error(String(cause));
169
+ setResult((current) => ({ ...current, state: "error", error }));
170
+ clearSocket();
171
+ if (reconnectAutomatically) scheduleReconnect();
172
+ };
173
+ const onOpen = (source) => {
174
+ if (disposed || source !== socket) return;
175
+ if (activeStream?.kind === "relay") {
176
+ const body = encodeStreamOpen({
177
+ channel: {
178
+ ...activeStream.channel,
179
+ kind: STREAM_KIND_BROWSER
180
+ },
181
+ token: activeStream.token,
182
+ role: STREAM_ROLE_CLIENT,
183
+ resumeFromSeq: lastRelaySequence === null ? "0" : (BigInt(lastRelaySequence) + 1n).toString()
184
+ });
185
+ try {
186
+ source.send(relayDatagram(RELAY_TAG_OPEN, body));
187
+ } catch (error) {
188
+ fail(error);
189
+ }
190
+ return;
191
+ }
192
+ setResult((current) => ({ ...current, state: "live", error: null }));
193
+ };
194
+ const drainLatestFrame = async () => {
195
+ if (decodingFrame) return;
196
+ decodingFrame = true;
197
+ try {
198
+ while (pendingFrame) {
199
+ if (disposed) break;
200
+ const pending = pendingFrame;
201
+ pendingFrame = null;
202
+ const { bytes, source } = pending;
203
+ const frame = decodeBrowserFrameMessage(bytes);
204
+ if (disposed || source !== socket) continue;
205
+ if (frame.browserSessionId !== browserSessionId || frame.targetId !== targetId) {
206
+ throw new Error("browser frame belongs to another resource");
207
+ }
208
+ if (!controllerGeneration || frame.controllerGeneration !== controllerGeneration) {
209
+ throw new Error("browser frame belongs to a stale controller");
210
+ }
211
+ const key = `${browserSessionId}:${targetId}:${frame.controllerGeneration}:${frame.targetGeneration}:${frame.documentGeneration}`;
212
+ if (latestRef.current.key === key && frame.sequence <= latestRef.current.sequence)
213
+ continue;
214
+ latestRef.current = { key, sequence: frame.sequence };
215
+ failures = 0;
216
+ setResult((current) => ({
217
+ ...current,
218
+ state: "live",
219
+ frame,
220
+ error: null
221
+ }));
222
+ }
223
+ } catch (cause) {
224
+ fail(cause);
225
+ } finally {
226
+ decodingFrame = false;
227
+ if (!disposed && pendingFrame) void drainLatestFrame();
228
+ }
229
+ };
230
+ const onMessage = (source, event) => {
231
+ void (async () => {
232
+ try {
233
+ const bytes = await messageBytes(event.data);
234
+ if (disposed || source !== socket) return;
235
+ let frameBytes = bytes;
236
+ if (activeStream?.kind === "relay") {
237
+ if (bytes.length < 1) throw new Error("browser relay returned an empty message");
238
+ const tag = bytes[0];
239
+ const body = bytes.subarray(1);
240
+ if (tag === RELAY_TAG_OPEN_ACK) {
241
+ const ack = decodeStreamOpenAck(body);
242
+ if (!ack.accepted) {
243
+ activeAttachment = null;
244
+ activeStream = null;
245
+ attachmentExpiresAt = 0;
246
+ throw new Error(ack.error?.message ?? "browser stream was rejected by the relay");
247
+ }
248
+ relayAccepted = true;
249
+ return;
250
+ }
251
+ if (tag === RELAY_TAG_CLOSE) {
252
+ activeAttachment = null;
253
+ activeStream = null;
254
+ attachmentExpiresAt = 0;
255
+ lastRelaySequence = null;
256
+ relayAccepted = false;
257
+ fail(new Error("Browser frame source ended."), false);
258
+ return;
259
+ }
260
+ if (tag !== RELAY_TAG_FRAME || !relayAccepted) return;
261
+ const relayFrame = decodeStreamFrame(body);
262
+ lastRelaySequence = relayFrame.seq;
263
+ frameBytes = relayFrame.data;
264
+ }
265
+ pendingFrame = { bytes: frameBytes, source };
266
+ void drainLatestFrame();
267
+ } catch (cause) {
268
+ fail(cause);
269
+ }
270
+ })();
271
+ };
272
+ const onError = (source) => {
273
+ if (source !== socket) return;
274
+ fail(new Error("Browser view lost connection."));
275
+ };
276
+ const onClose = (source) => {
277
+ if (disposed || source !== socket) return;
278
+ clearSocket();
279
+ scheduleReconnect();
280
+ };
281
+ const connectSocket = () => {
282
+ if (disposed) return;
283
+ clearSocket();
284
+ if (!activeAttachment || !activeStream || attachmentExpiresAt <= Date.now() + 1e3) {
285
+ void attach();
286
+ return;
287
+ }
288
+ relayAccepted = false;
289
+ setResult((current) => ({
290
+ ...current,
291
+ state: "connecting",
292
+ error: null
293
+ }));
294
+ const createSocket = factoryRef.current ?? ((url, protocols) => new WebSocket(url, protocols));
295
+ const openedSocket = createSocket(
296
+ activeStream.kind === "relay" ? activeStream.url : browserFrameSocketUrl(activeAttachment, streamRef.current),
297
+ activeStream.kind === "direct_websocket" ? [...activeStream.protocols] : []
298
+ );
299
+ socket = openedSocket;
300
+ socketHandlers = {
301
+ open: () => onOpen(openedSocket),
302
+ message: (event) => onMessage(openedSocket, event),
303
+ error: () => onError(openedSocket),
304
+ close: () => onClose(openedSocket)
305
+ };
306
+ openedSocket.binaryType = "arraybuffer";
307
+ openedSocket.addEventListener("open", socketHandlers.open);
308
+ openedSocket.addEventListener("message", socketHandlers.message);
309
+ openedSocket.addEventListener("error", socketHandlers.error);
310
+ openedSocket.addEventListener("close", socketHandlers.close);
311
+ };
312
+ const attach = async () => {
313
+ if (disposed) return;
314
+ clearSocket();
315
+ if (expiryTimer) clearTimeout(expiryTimer);
316
+ setResult((current) => ({
317
+ ...current,
318
+ state: current.attachment ? "reconnecting" : "attaching",
319
+ error: null
320
+ }));
321
+ try {
322
+ const attachment = await client.attachBrowserSession(
323
+ workspaceId,
324
+ browserSessionId,
325
+ {
326
+ targetId,
327
+ expiresInSeconds: 120,
328
+ ...streamRef.current ? { stream: streamRef.current } : {}
329
+ },
330
+ { signal: attachmentAbort.signal }
331
+ );
332
+ if (disposed) return;
333
+ if (attachment.browserSessionId !== browserSessionId || attachment.targetId !== targetId) {
334
+ throw new Error("browser attachment does not match the requested resource");
335
+ }
336
+ controllerGeneration = attachment.controllerGeneration;
337
+ activeAttachment = attachment;
338
+ activeStream = attachment.stream;
339
+ attachmentExpiresAt = Date.parse(attachment.expiresAt);
340
+ lastRelaySequence = null;
341
+ relayAccepted = false;
342
+ setResult((current) => ({
343
+ ...current,
344
+ state: "connecting",
345
+ attachment: {
346
+ browserSessionId: attachment.browserSessionId,
347
+ controllerGeneration: attachment.controllerGeneration,
348
+ targetId: attachment.targetId,
349
+ expiresAt: attachment.expiresAt
350
+ },
351
+ error: null
352
+ }));
353
+ const refreshIn = Number.isFinite(attachmentExpiresAt) ? Math.max(1e3, attachmentExpiresAt - Date.now() - 5e3) : 6e4;
354
+ expiryTimer = setTimeout(() => {
355
+ if (disposed) return;
356
+ clearSocket(true);
357
+ activeAttachment = null;
358
+ activeStream = null;
359
+ attachmentExpiresAt = 0;
360
+ void attach();
361
+ }, refreshIn);
362
+ connectSocket();
363
+ } catch (cause) {
364
+ if (attachmentAbort.signal.aborted || disposed) return;
365
+ fail(cause, !isPlacementGenerationLossError(cause));
366
+ }
367
+ };
368
+ void attach();
369
+ return () => {
370
+ disposed = true;
371
+ attachmentAbort.abort();
372
+ if (reconnectTimer) clearTimeout(reconnectTimer);
373
+ if (expiryTimer) clearTimeout(expiryTimer);
374
+ clearSocket(true);
375
+ };
376
+ }, [client, enabled, nonce, options.browserSessionId, options.targetId, workspaceId]);
377
+ return { ...result, reconnect };
378
+ }
379
+ function relayDatagram(tag, body) {
380
+ const message = new Uint8Array(body.length + 1);
381
+ message[0] = tag;
382
+ message.set(body, 1);
383
+ return message.buffer;
384
+ }
385
+ async function messageBytes(value) {
386
+ if (value instanceof ArrayBuffer) return new Uint8Array(value);
387
+ if (ArrayBuffer.isView(value)) {
388
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength).slice();
389
+ }
390
+ if (typeof Blob !== "undefined" && value instanceof Blob) {
391
+ return new Uint8Array(await value.arrayBuffer());
392
+ }
393
+ throw new Error("browser frame stream returned a non-binary message");
394
+ }
395
+ function isPlacementGenerationLossError(cause) {
396
+ const message = cause instanceof Error ? cause.message : String(cause);
397
+ return /placement instance changed|controller authority changed/i.test(message);
398
+ }
399
+
400
+ // src/hooks/use-attached-browsers.ts
401
+ import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
402
+ function useAttachedBrowsers(options = {}) {
403
+ const { client, workspaceId, workspaceInteractionEvent, workspaceInteractionConnectionState } = useEmbeddedBrowserInteraction(options);
404
+ const enabled = options.enabled ?? true;
405
+ const includeDisconnected = options.includeDisconnected ?? false;
406
+ const [state, setState] = useState2(() => emptyState(workspaceId, enabled));
407
+ const requestRef = useRef2({
408
+ id: 0,
409
+ controller: null
410
+ });
411
+ const mountedRef = useRef2(true);
412
+ const visible = state.workspaceId === workspaceId ? state : emptyState(workspaceId, enabled);
413
+ const cancelLoad = useCallback2(() => {
414
+ const id = requestRef.current.id + 1;
415
+ requestRef.current.controller?.abort();
416
+ requestRef.current = { id, controller: null };
417
+ }, []);
418
+ const load = useCallback2(
419
+ async (foreground) => {
420
+ if (!enabled) return;
421
+ const id = requestRef.current.id + 1;
422
+ requestRef.current.controller?.abort();
423
+ const controller = new AbortController();
424
+ requestRef.current = { id, controller };
425
+ setState((current) => ({
426
+ ...current.workspaceId === workspaceId ? current : emptyState(workspaceId, true),
427
+ loading: foreground,
428
+ refreshing: !foreground,
429
+ error: foreground || current.workspaceId !== workspaceId ? null : current.error
430
+ }));
431
+ try {
432
+ const response = await client.listAttachedBrowsers(workspaceId, {
433
+ includeDisconnected,
434
+ signal: controller.signal
435
+ });
436
+ if (!mountedRef.current || requestRef.current.id !== id) return;
437
+ setState({
438
+ workspaceId,
439
+ revision: response.revision,
440
+ bridges: response.bridges,
441
+ devices: sortDevices(response.devices),
442
+ loading: false,
443
+ refreshing: false,
444
+ error: null
445
+ });
446
+ } catch (cause) {
447
+ if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id)
448
+ return;
449
+ setState((current) => ({
450
+ ...current.workspaceId === workspaceId ? current : emptyState(workspaceId, true),
451
+ loading: false,
452
+ refreshing: false,
453
+ error: cause instanceof Error ? cause : new Error(String(cause))
454
+ }));
455
+ } finally {
456
+ if (requestRef.current.id === id) requestRef.current = { id, controller: null };
457
+ }
458
+ },
459
+ [client, enabled, includeDisconnected, workspaceId]
460
+ );
461
+ const refresh = useCallback2(async () => await load(false), [load]);
462
+ useEffect2(() => {
463
+ mountedRef.current = true;
464
+ if (!enabled) {
465
+ cancelLoad();
466
+ setState(emptyState(workspaceId, false));
467
+ return;
468
+ }
469
+ void load(true);
470
+ return () => {
471
+ mountedRef.current = false;
472
+ cancelLoad();
473
+ };
474
+ }, [cancelLoad, enabled, load, workspaceId]);
475
+ useInteractionInvalidation({
476
+ workspaceId,
477
+ key: `${workspaceId}:${includeDisconnected}`,
478
+ enabled,
479
+ event: workspaceInteractionEvent,
480
+ connectionState: workspaceInteractionConnectionState,
481
+ knownRevision: visible.revision,
482
+ refresh,
483
+ fallbackPollIntervalMs: options.pollIntervalMs
484
+ });
485
+ return {
486
+ revision: visible.revision,
487
+ bridges: visible.bridges,
488
+ devices: visible.devices,
489
+ loading: visible.loading,
490
+ refreshing: visible.refreshing,
491
+ error: visible.error,
492
+ refresh
493
+ };
494
+ }
495
+ function emptyState(workspaceId, loading) {
496
+ return {
497
+ workspaceId,
498
+ revision: 0,
499
+ bridges: [],
500
+ devices: [],
501
+ loading,
502
+ refreshing: false,
503
+ error: null
504
+ };
505
+ }
506
+ function sortDevices(devices) {
507
+ return [...devices].sort(
508
+ (left, right) => Number(right.state === "connected") - Number(left.state === "connected") || timestamp(right.lastSeenAt) - timestamp(left.lastSeenAt) || left.name.localeCompare(right.name)
509
+ );
510
+ }
511
+ function timestamp(value) {
512
+ const parsed = Date.parse(value);
513
+ return Number.isFinite(parsed) ? parsed : 0;
514
+ }
515
+
516
+ // src/hooks/use-browser-identities.ts
517
+ import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
518
+ function useBrowserIdentities(options = {}) {
519
+ const { client, workspaceId, workspaceInteractionEvent, workspaceInteractionConnectionState } = useEmbeddedBrowserInteraction(options);
520
+ const enabled = options.enabled ?? true;
521
+ const includeArchived = options.includeArchived ?? false;
522
+ const [state, setState] = useState3(() => emptyState2(workspaceId, enabled));
523
+ const [mutationCount, setMutationCount] = useState3(0);
524
+ const requestRef = useRef3({
525
+ id: 0,
526
+ controller: null
527
+ });
528
+ const mountedRef = useRef3(true);
529
+ const visibleState = state.workspaceId === workspaceId ? state : emptyState2(workspaceId, enabled);
530
+ const cancelLoad = useCallback3(() => {
531
+ const id = requestRef.current.id + 1;
532
+ requestRef.current.controller?.abort();
533
+ requestRef.current = { id, controller: null };
534
+ }, []);
535
+ const load = useCallback3(
536
+ async (foreground) => {
537
+ if (!enabled) return;
538
+ const id = requestRef.current.id + 1;
539
+ requestRef.current.controller?.abort();
540
+ const controller = new AbortController();
541
+ requestRef.current = { id, controller };
542
+ setState((current) => ({
543
+ ...current.workspaceId === workspaceId ? current : emptyState2(workspaceId, true),
544
+ loading: foreground,
545
+ refreshing: !foreground,
546
+ error: foreground || current.workspaceId !== workspaceId ? null : current.error
547
+ }));
548
+ try {
549
+ const response = await client.listBrowserIdentities(workspaceId, {
550
+ includeArchived,
551
+ signal: controller.signal
552
+ });
553
+ if (!mountedRef.current || requestRef.current.id !== id) return;
554
+ setState({
555
+ workspaceId,
556
+ revision: response.revision,
557
+ identities: sortIdentities(response.identities),
558
+ loading: false,
559
+ refreshing: false,
560
+ error: null
561
+ });
562
+ } catch (cause) {
563
+ if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id)
564
+ return;
565
+ setState((current) => ({
566
+ ...current.workspaceId === workspaceId ? current : emptyState2(workspaceId, true),
567
+ loading: false,
568
+ refreshing: false,
569
+ error: cause instanceof Error ? cause : new Error(String(cause))
570
+ }));
571
+ } finally {
572
+ if (requestRef.current.id === id) requestRef.current = { id, controller: null };
573
+ }
574
+ },
575
+ [client, enabled, includeArchived, workspaceId]
576
+ );
577
+ const refresh = useCallback3(async () => await load(false), [load]);
578
+ useEffect3(() => {
579
+ mountedRef.current = true;
580
+ if (!enabled) {
581
+ cancelLoad();
582
+ setState(emptyState2(workspaceId, false));
583
+ return;
584
+ }
585
+ void load(true);
586
+ return () => {
587
+ mountedRef.current = false;
588
+ cancelLoad();
589
+ };
590
+ }, [cancelLoad, enabled, load, workspaceId]);
591
+ useInteractionInvalidation({
592
+ workspaceId,
593
+ key: workspaceId,
594
+ enabled,
595
+ event: workspaceInteractionEvent,
596
+ connectionState: workspaceInteractionConnectionState,
597
+ knownRevision: visibleState.revision,
598
+ refresh
599
+ });
600
+ const mutate = useCallback3(async (operation) => {
601
+ setMutationCount((count) => count + 1);
602
+ try {
603
+ return await operation();
604
+ } finally {
605
+ if (mountedRef.current) setMutationCount((count) => Math.max(0, count - 1));
606
+ }
607
+ }, []);
608
+ const create = useCallback3(
609
+ async (request) => await mutate(async () => {
610
+ const response = await client.createBrowserIdentity(workspaceId, {
611
+ ...request,
612
+ operationId: request.operationId ?? crypto.randomUUID()
613
+ });
614
+ setState((current) => mergeIdentity(current, workspaceId, response.identity));
615
+ return response;
616
+ }),
617
+ [client, mutate, workspaceId]
618
+ );
619
+ const revisions = useCallback3(
620
+ async (identityId) => await client.listBrowserRevisions(workspaceId, identityId),
621
+ [client, workspaceId]
622
+ );
623
+ const update = useCallback3(
624
+ async (identityId, request) => await mutate(async () => {
625
+ const response = await client.updateBrowserIdentity(workspaceId, identityId, {
626
+ ...request,
627
+ operationId: request.operationId ?? crypto.randomUUID()
628
+ });
629
+ setState(
630
+ (current) => mergeIdentity(current, workspaceId, response.identity, includeArchived)
631
+ );
632
+ return response;
633
+ }),
634
+ [client, includeArchived, mutate, workspaceId]
635
+ );
636
+ const publish = useCallback3(
637
+ async (browserSessionId, request) => await mutate(async () => {
638
+ const response = await client.publishBrowserRevision(workspaceId, browserSessionId, {
639
+ ...request,
640
+ operationId: request.operationId ?? crypto.randomUUID()
641
+ });
642
+ setState((current) => mergeIdentity(current, workspaceId, response.identity));
643
+ return response;
644
+ }),
645
+ [client, mutate, workspaceId]
646
+ );
647
+ return {
648
+ revision: visibleState.revision,
649
+ identities: visibleState.identities,
650
+ loading: visibleState.loading,
651
+ refreshing: visibleState.refreshing,
652
+ mutating: mutationCount > 0,
653
+ error: visibleState.error,
654
+ refresh,
655
+ create,
656
+ update,
657
+ revisions,
658
+ publish
659
+ };
660
+ }
661
+ function emptyState2(workspaceId, loading) {
662
+ return {
663
+ workspaceId,
664
+ revision: 0,
665
+ identities: [],
666
+ loading,
667
+ refreshing: false,
668
+ error: null
669
+ };
670
+ }
671
+ function sortIdentities(identities) {
672
+ return [...identities].sort(
673
+ (left, right) => timestamp2(right.updatedAt) - timestamp2(left.updatedAt) || left.name.localeCompare(right.name)
674
+ );
675
+ }
676
+ function timestamp2(value) {
677
+ const parsed = Date.parse(value);
678
+ return Number.isFinite(parsed) ? parsed : 0;
679
+ }
680
+ function mergeIdentity(current, workspaceId, identity, includeArchived = false) {
681
+ if (current.workspaceId !== workspaceId) return current;
682
+ return {
683
+ ...current,
684
+ identities: sortIdentities(
685
+ identity.status === "archived" && !includeArchived ? current.identities.filter((candidate) => candidate.id !== identity.id) : [...current.identities.filter((candidate) => candidate.id !== identity.id), identity]
686
+ ),
687
+ error: null
688
+ };
689
+ }
690
+
691
+ // src/hooks/use-browser-downloads.ts
692
+ import { useCallback as useCallback4, useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
693
+ function useBrowserDownloads(options) {
694
+ const { client, workspaceId } = useEmbeddedBrowserInteraction(options);
695
+ const browserSessionId = options.browserSessionId;
696
+ const enabled = (options.enabled ?? true) && browserSessionId !== null;
697
+ const pageLive = usePageLiveActivity();
698
+ const pollIntervalMs = Math.max(750, options.pollIntervalMs ?? 2e3);
699
+ const scope = browserSessionId ?? "none";
700
+ const [state, setState] = useState4(() => emptyState3(scope, enabled));
701
+ const [saving, setSaving] = useState4({});
702
+ const visible = state.scope === scope ? state : emptyState3(scope, enabled);
703
+ const requestRef = useRef4({
704
+ id: 0,
705
+ controller: null
706
+ });
707
+ const mountedRef = useRef4(true);
708
+ const retryOperationsRef = useRef4(/* @__PURE__ */ new Map());
709
+ const cancelLoad = useCallback4(() => {
710
+ requestRef.current.controller?.abort();
711
+ requestRef.current = { id: requestRef.current.id + 1, controller: null };
712
+ }, []);
713
+ const load = useCallback4(
714
+ async (foreground) => {
715
+ if (!enabled || !browserSessionId) return;
716
+ const id = requestRef.current.id + 1;
717
+ requestRef.current.controller?.abort();
718
+ const controller = new AbortController();
719
+ requestRef.current = { id, controller };
720
+ setState((current) => ({
721
+ ...current.scope === scope ? current : emptyState3(scope, true),
722
+ loading: foreground,
723
+ refreshing: !foreground,
724
+ error: foreground || current.scope !== scope ? null : current.error
725
+ }));
726
+ try {
727
+ const response = await client.listBrowserDownloads(workspaceId, browserSessionId, {
728
+ signal: controller.signal
729
+ });
730
+ if (!mountedRef.current || requestRef.current.id !== id) return;
731
+ setState({
732
+ scope,
733
+ downloads: sortDownloads(response.downloads),
734
+ loading: false,
735
+ refreshing: false,
736
+ error: null
737
+ });
738
+ } catch (cause) {
739
+ if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id)
740
+ return;
741
+ setState((current) => ({
742
+ ...current.scope === scope ? current : emptyState3(scope, true),
743
+ loading: false,
744
+ refreshing: false,
745
+ error: asError(cause)
746
+ }));
747
+ } finally {
748
+ if (requestRef.current.id === id) requestRef.current = { id, controller: null };
749
+ }
750
+ },
751
+ [browserSessionId, client, enabled, scope, workspaceId]
752
+ );
753
+ const refresh = useCallback4(async () => await load(false), [load]);
754
+ useEffect4(() => {
755
+ retryOperationsRef.current.clear();
756
+ setSaving({});
757
+ }, [scope]);
758
+ useEffect4(() => {
759
+ mountedRef.current = true;
760
+ if (!enabled) {
761
+ cancelLoad();
762
+ setState(emptyState3(scope, false));
763
+ return () => {
764
+ mountedRef.current = false;
765
+ cancelLoad();
766
+ };
767
+ }
768
+ void load(true);
769
+ return () => {
770
+ mountedRef.current = false;
771
+ cancelLoad();
772
+ };
773
+ }, [cancelLoad, enabled, load, scope]);
774
+ useEffect4(() => {
775
+ if (!enabled || !pageLive || Object.keys(saving).length > 0) return;
776
+ const timer = setInterval(() => {
777
+ if (!requestRef.current.controller) void refresh();
778
+ }, pollIntervalMs);
779
+ return () => clearInterval(timer);
780
+ }, [enabled, pageLive, pollIntervalMs, refresh, saving]);
781
+ const saveToWorkspace = useCallback4(
782
+ async (downloadId, destinationPath, saveOptions = {}) => {
783
+ if (!browserSessionId) throw new Error("No BrowserSession is selected.");
784
+ const overwrite = saveOptions.overwrite ?? false;
785
+ const attemptKey = `${browserSessionId}\0${downloadId}\0${destinationPath}\0${overwrite}`;
786
+ const operationId = saveOptions.operationId ?? retryOperationsRef.current.get(attemptKey) ?? crypto.randomUUID();
787
+ retryOperationsRef.current.set(attemptKey, operationId);
788
+ setSaving((current) => ({ ...current, [downloadId]: (current[downloadId] ?? 0) + 1 }));
789
+ try {
790
+ const response = await client.saveBrowserDownload(
791
+ workspaceId,
792
+ browserSessionId,
793
+ downloadId,
794
+ { operationId, destinationPath, overwrite }
795
+ );
796
+ retryOperationsRef.current.delete(attemptKey);
797
+ if (mountedRef.current) {
798
+ setState(
799
+ (current) => current.scope === scope ? {
800
+ ...current,
801
+ downloads: sortDownloads([
802
+ ...current.downloads.filter((download) => download.id !== response.download.id),
803
+ response.download
804
+ ]),
805
+ error: null
806
+ } : current
807
+ );
808
+ }
809
+ return response;
810
+ } finally {
811
+ if (mountedRef.current) {
812
+ setSaving((current) => {
813
+ const count = (current[downloadId] ?? 1) - 1;
814
+ if (count > 0) return { ...current, [downloadId]: count };
815
+ const { [downloadId]: _removed, ...rest } = current;
816
+ return rest;
817
+ });
818
+ }
819
+ }
820
+ },
821
+ [browserSessionId, client, scope, workspaceId]
822
+ );
823
+ return {
824
+ downloads: visible.downloads,
825
+ loading: visible.loading,
826
+ refreshing: visible.refreshing,
827
+ savingDownloadIds: Object.keys(saving),
828
+ error: visible.error,
829
+ refresh,
830
+ saveToWorkspace
831
+ };
832
+ }
833
+ function emptyState3(scope, loading) {
834
+ return {
835
+ scope,
836
+ downloads: [],
837
+ loading,
838
+ refreshing: false,
839
+ error: null
840
+ };
841
+ }
842
+ function sortDownloads(downloads) {
843
+ return [...downloads].sort(
844
+ (left, right) => right.startedAt.localeCompare(left.startedAt) || left.id.localeCompare(right.id)
845
+ );
846
+ }
847
+ function asError(cause) {
848
+ return cause instanceof Error ? cause : new Error(String(cause));
849
+ }
850
+
851
+ // src/hooks/use-browser-session.ts
852
+ import { OpenGeniApiError } from "@opengeni/sdk";
853
+ import { useCallback as useCallback5, useEffect as useEffect5, useRef as useRef5, useState as useState5 } from "react";
854
+ function useBrowserSession(options) {
855
+ const { client, workspaceId } = useEmbeddedBrowserInteraction(options);
856
+ const browserSessionId = options.browserSessionId;
857
+ const enabled = (options.enabled ?? true) && browserSessionId !== null;
858
+ const pageLive = usePageLiveActivity();
859
+ const pollIntervalMs = Math.max(750, options.pollIntervalMs ?? 2e3);
860
+ const [state, setState] = useState5(() => emptyState4(browserSessionId, enabled));
861
+ const visible = state.browserSessionId === browserSessionId ? state : emptyState4(browserSessionId, enabled);
862
+ const refreshBlocked = isNonRetryableInteractionError(visible.error);
863
+ const selectedTargetIdRef = useRef5(visible.selectedTargetId);
864
+ selectedTargetIdRef.current = visible.selectedTargetId;
865
+ const observationRef = useRef5({ browserSessionId, observation: visible.observation });
866
+ if (observationRef.current.browserSessionId !== browserSessionId) {
867
+ observationRef.current = {
868
+ browserSessionId,
869
+ observation: visible.observation
870
+ };
871
+ } else {
872
+ observationRef.current.observation = visible.observation;
873
+ }
874
+ const requestRef = useRef5({
875
+ id: 0,
876
+ controller: null
877
+ });
878
+ const mutationRef = useRef5({
879
+ browserSessionId,
880
+ count: 0
881
+ });
882
+ const mountedRef = useRef5(true);
883
+ const invalidateRefresh = useCallback5(() => {
884
+ const id = requestRef.current.id + 1;
885
+ requestRef.current.controller?.abort();
886
+ requestRef.current = { id, controller: null };
887
+ }, []);
888
+ const refresh = useCallback5(async () => {
889
+ if (!enabled || !browserSessionId) return;
890
+ const id = requestRef.current.id + 1;
891
+ requestRef.current.controller?.abort();
892
+ const controller = new AbortController();
893
+ requestRef.current = { id, controller };
894
+ try {
895
+ const [session, targetResponse] = await Promise.all([
896
+ client.getBrowserSession(workspaceId, browserSessionId, {
897
+ signal: controller.signal
898
+ }),
899
+ client.listBrowserTargets(workspaceId, browserSessionId, {
900
+ signal: controller.signal
901
+ })
902
+ ]);
903
+ if (!mountedRef.current || requestRef.current.id !== id) return;
904
+ const selected = chooseTarget(targetResponse.targets, selectedTargetIdRef.current);
905
+ selectedTargetIdRef.current = selected?.id ?? null;
906
+ observationRef.current = { browserSessionId, observation: null };
907
+ setState(
908
+ (current) => current.browserSessionId === browserSessionId ? {
909
+ ...current,
910
+ session,
911
+ targets: targetResponse.targets,
912
+ selectedTargetId: selected?.id ?? null,
913
+ observation: null,
914
+ loading: false,
915
+ error: null
916
+ } : current
917
+ );
918
+ if (!selected) return;
919
+ try {
920
+ const observation = await client.observeBrowserTarget(
921
+ workspaceId,
922
+ browserSessionId,
923
+ selected.id,
924
+ { signal: controller.signal }
925
+ );
926
+ if (!mountedRef.current || requestRef.current.id !== id) return;
927
+ observationRef.current = { browserSessionId, observation };
928
+ setState(
929
+ (current) => current.browserSessionId === browserSessionId && current.selectedTargetId === observation.target.id ? { ...current, observation } : current
930
+ );
931
+ } catch (cause) {
932
+ if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id)
933
+ return;
934
+ void cause;
935
+ }
936
+ } catch (cause) {
937
+ if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id) return;
938
+ setState(
939
+ (current) => current.browserSessionId === browserSessionId ? {
940
+ ...current,
941
+ loading: false,
942
+ error: cause instanceof Error ? cause : new Error(String(cause))
943
+ } : current
944
+ );
945
+ } finally {
946
+ if (requestRef.current.id === id) {
947
+ requestRef.current = { id, controller: null };
948
+ }
949
+ }
950
+ }, [browserSessionId, client, enabled, workspaceId]);
951
+ useEffect5(() => {
952
+ mountedRef.current = true;
953
+ if (!enabled) {
954
+ invalidateRefresh();
955
+ setState(emptyState4(browserSessionId, false));
956
+ return;
957
+ }
958
+ setState(
959
+ (current) => current.browserSessionId === browserSessionId ? { ...current, loading: true } : emptyState4(browserSessionId, true)
960
+ );
961
+ void refresh();
962
+ return () => {
963
+ mountedRef.current = false;
964
+ invalidateRefresh();
965
+ };
966
+ }, [browserSessionId, enabled, invalidateRefresh, refresh]);
967
+ useEffect5(() => {
968
+ if (!enabled || !pageLive || visible.mutating || refreshBlocked) return;
969
+ const timer = setInterval(() => {
970
+ if (!requestRef.current.controller) void refresh();
971
+ }, pollIntervalMs);
972
+ return () => clearInterval(timer);
973
+ }, [enabled, pageLive, pollIntervalMs, refresh, refreshBlocked, visible.mutating]);
974
+ useEffect5(() => {
975
+ if (!enabled || !browserSessionId || !pageLive || refreshBlocked) return;
976
+ let disposed = false;
977
+ const heartbeat = () => {
978
+ void client.heartbeatBrowserSession(workspaceId, browserSessionId).catch(() => {
979
+ if (!disposed) void refresh();
980
+ });
981
+ };
982
+ const timer = setInterval(heartbeat, 3e4);
983
+ return () => {
984
+ disposed = true;
985
+ clearInterval(timer);
986
+ };
987
+ }, [browserSessionId, client, enabled, pageLive, refresh, refreshBlocked, workspaceId]);
988
+ const runMutation = useCallback5(
989
+ async (scopeBrowserSessionId, operation) => {
990
+ invalidateRefresh();
991
+ if (mutationRef.current.browserSessionId !== scopeBrowserSessionId) {
992
+ mutationRef.current = {
993
+ browserSessionId: scopeBrowserSessionId,
994
+ count: 0
995
+ };
996
+ }
997
+ mutationRef.current.count += 1;
998
+ setState(
999
+ (current) => current.browserSessionId === scopeBrowserSessionId ? { ...current, mutating: true, error: null } : current
1000
+ );
1001
+ try {
1002
+ return await operation();
1003
+ } catch (cause) {
1004
+ const error = cause instanceof Error ? cause : new Error(String(cause));
1005
+ setState(
1006
+ (current) => current.browserSessionId === scopeBrowserSessionId ? { ...current, error } : current
1007
+ );
1008
+ throw error;
1009
+ } finally {
1010
+ if (mutationRef.current.browserSessionId === scopeBrowserSessionId) {
1011
+ mutationRef.current.count = Math.max(0, mutationRef.current.count - 1);
1012
+ const mutating = mutationRef.current.count > 0;
1013
+ setState(
1014
+ (current) => current.browserSessionId === scopeBrowserSessionId ? { ...current, mutating } : current
1015
+ );
1016
+ }
1017
+ }
1018
+ },
1019
+ [invalidateRefresh]
1020
+ );
1021
+ const selectTarget = useCallback5(
1022
+ async (targetId) => {
1023
+ if (!browserSessionId) throw new Error("No BrowserSession is selected.");
1024
+ return await runMutation(browserSessionId, async () => {
1025
+ let targets = visible.targets;
1026
+ let target;
1027
+ let observation;
1028
+ try {
1029
+ observation = await client.selectBrowserTarget(workspaceId, browserSessionId, targetId);
1030
+ target = observation.target;
1031
+ } catch (cause) {
1032
+ if (!isMissingBrowserTarget(cause)) throw cause;
1033
+ const response = await client.listBrowserTargets(workspaceId, browserSessionId);
1034
+ targets = response.targets;
1035
+ const fallback = chooseTarget(targets, null);
1036
+ if (!fallback) {
1037
+ selectedTargetIdRef.current = null;
1038
+ observationRef.current = { browserSessionId, observation: null };
1039
+ setState(
1040
+ (current) => current.browserSessionId === browserSessionId ? {
1041
+ ...current,
1042
+ targets,
1043
+ selectedTargetId: null,
1044
+ observation: null
1045
+ } : current
1046
+ );
1047
+ throw cause;
1048
+ }
1049
+ observation = await client.selectBrowserTarget(
1050
+ workspaceId,
1051
+ browserSessionId,
1052
+ fallback.id
1053
+ );
1054
+ target = observation.target;
1055
+ }
1056
+ selectedTargetIdRef.current = target.id;
1057
+ observationRef.current = { browserSessionId, observation };
1058
+ setState(
1059
+ (current) => current.browserSessionId === browserSessionId ? {
1060
+ ...current,
1061
+ targets: targets.map((candidate) => ({
1062
+ ...candidate,
1063
+ selected: candidate.id === target.id
1064
+ })),
1065
+ selectedTargetId: target.id,
1066
+ observation
1067
+ } : current
1068
+ );
1069
+ return target;
1070
+ });
1071
+ },
1072
+ [browserSessionId, client, runMutation, visible.targets, workspaceId]
1073
+ );
1074
+ const openTarget = useCallback5(
1075
+ async (url) => {
1076
+ if (!browserSessionId) throw new Error("No BrowserSession is selected.");
1077
+ return await runMutation(browserSessionId, async () => {
1078
+ const observation = await client.openBrowserTarget(
1079
+ workspaceId,
1080
+ browserSessionId,
1081
+ url === void 0 ? {} : { url }
1082
+ );
1083
+ const target = observation.target;
1084
+ selectedTargetIdRef.current = target.id;
1085
+ observationRef.current = { browserSessionId, observation };
1086
+ setState(
1087
+ (current) => current.browserSessionId === browserSessionId ? {
1088
+ ...current,
1089
+ targets: [
1090
+ ...current.targets.map((candidate) => ({
1091
+ ...candidate,
1092
+ selected: false
1093
+ })),
1094
+ target
1095
+ ],
1096
+ selectedTargetId: target.id,
1097
+ observation
1098
+ } : current
1099
+ );
1100
+ return target;
1101
+ });
1102
+ },
1103
+ [browserSessionId, client, runMutation, workspaceId]
1104
+ );
1105
+ const closeTarget = useCallback5(
1106
+ async (targetId) => {
1107
+ if (!browserSessionId) throw new Error("No BrowserSession is selected.");
1108
+ await runMutation(browserSessionId, async () => {
1109
+ const response = await client.closeBrowserTarget(workspaceId, browserSessionId, targetId);
1110
+ const selected = chooseTarget(response.targets, null);
1111
+ const observation = selected ? await client.observeBrowserTarget(workspaceId, browserSessionId, selected.id) : null;
1112
+ selectedTargetIdRef.current = selected?.id ?? null;
1113
+ observationRef.current = { browserSessionId, observation };
1114
+ setState(
1115
+ (current) => current.browserSessionId === browserSessionId ? {
1116
+ ...current,
1117
+ targets: response.targets,
1118
+ selectedTargetId: selected?.id ?? null,
1119
+ observation
1120
+ } : current
1121
+ );
1122
+ });
1123
+ },
1124
+ [browserSessionId, client, runMutation, workspaceId]
1125
+ );
1126
+ const dispatchAction = useCallback5(
1127
+ async (action, operationId, frame) => {
1128
+ if (!browserSessionId) throw new Error("No BrowserSession is selected.");
1129
+ if (frame && frame.browserSessionId !== browserSessionId) {
1130
+ throw new Error("The displayed browser frame belongs to another BrowserSession.");
1131
+ }
1132
+ const currentObservation = observationRef.current.browserSessionId === browserSessionId ? observationRef.current.observation : null;
1133
+ const fence = frame ? {
1134
+ targetId: frame.targetId,
1135
+ expectedTargetGeneration: frame.targetGeneration,
1136
+ expectedDocumentGeneration: frame.documentGeneration,
1137
+ expectedFrameId: frame.frameId
1138
+ } : currentObservation ? {
1139
+ targetId: currentObservation.target.id,
1140
+ expectedTargetGeneration: currentObservation.target.targetGeneration,
1141
+ expectedDocumentGeneration: currentObservation.target.documentGeneration,
1142
+ expectedFrameId: currentObservation.frameId
1143
+ } : null;
1144
+ if (!fence) {
1145
+ throw new Error("The browser page is not ready for input.");
1146
+ }
1147
+ try {
1148
+ const receipt = await client.actInBrowser(workspaceId, browserSessionId, {
1149
+ operationId,
1150
+ ...fence,
1151
+ observationMode: "none",
1152
+ action
1153
+ });
1154
+ if (receipt.observation) {
1155
+ observationRef.current = {
1156
+ browserSessionId,
1157
+ observation: receipt.observation
1158
+ };
1159
+ setState(
1160
+ (current) => current.browserSessionId === browserSessionId ? {
1161
+ ...current,
1162
+ observation: receipt.observation,
1163
+ targets: replaceTarget(current.targets, receipt.observation.target)
1164
+ } : current
1165
+ );
1166
+ }
1167
+ return receipt;
1168
+ } catch (cause) {
1169
+ const error = cause instanceof Error ? cause : new Error(String(cause));
1170
+ setState(
1171
+ (current) => current.browserSessionId === browserSessionId ? { ...current, error } : current
1172
+ );
1173
+ throw error;
1174
+ }
1175
+ },
1176
+ [browserSessionId, client, workspaceId]
1177
+ );
1178
+ const act = useCallback5(
1179
+ async (action, operationId = crypto.randomUUID()) => await dispatchAction(action, operationId, null),
1180
+ [dispatchAction]
1181
+ );
1182
+ const actFromFrame = useCallback5(
1183
+ async (action, frame, operationId = crypto.randomUUID()) => await dispatchAction(action, operationId, frame),
1184
+ [dispatchAction]
1185
+ );
1186
+ const diagnostics = useCallback5(
1187
+ async (diagnosticOptions = {}) => {
1188
+ const targetId = selectedTargetIdRef.current;
1189
+ if (!browserSessionId || !targetId) {
1190
+ throw new Error("No browser tab is selected.");
1191
+ }
1192
+ return await client.listBrowserDiagnostics(
1193
+ workspaceId,
1194
+ browserSessionId,
1195
+ targetId,
1196
+ diagnosticOptions
1197
+ );
1198
+ },
1199
+ [browserSessionId, client, workspaceId]
1200
+ );
1201
+ const readClipboard = useCallback5(async () => {
1202
+ if (!browserSessionId) throw new Error("No BrowserSession is selected.");
1203
+ const clipboard = await client.readBrowserClipboard(workspaceId, browserSessionId);
1204
+ if (clipboard.browserSessionId !== browserSessionId) {
1205
+ throw new Error("Browser clipboard belongs to another BrowserSession.");
1206
+ }
1207
+ return clipboard;
1208
+ }, [browserSessionId, client, workspaceId]);
1209
+ return {
1210
+ session: visible.session,
1211
+ targets: visible.targets,
1212
+ selectedTarget: visible.targets.find((target) => target.id === visible.selectedTargetId) ?? null,
1213
+ observation: visible.observation,
1214
+ loading: visible.loading,
1215
+ mutating: visible.mutating,
1216
+ error: visible.error,
1217
+ refresh,
1218
+ selectTarget,
1219
+ openTarget,
1220
+ closeTarget,
1221
+ act,
1222
+ actFromFrame,
1223
+ readClipboard,
1224
+ diagnostics
1225
+ };
1226
+ }
1227
+ function emptyState4(browserSessionId, loading) {
1228
+ return {
1229
+ browserSessionId,
1230
+ session: null,
1231
+ targets: [],
1232
+ selectedTargetId: null,
1233
+ observation: null,
1234
+ loading,
1235
+ mutating: false,
1236
+ error: null
1237
+ };
1238
+ }
1239
+ function chooseTarget(targets, preferredId) {
1240
+ return targets.find((target) => target.id === preferredId) ?? targets.find((target) => target.selected && target.kind === "page") ?? targets.find((target) => target.kind === "page") ?? targets[0] ?? null;
1241
+ }
1242
+ function replaceTarget(targets, target) {
1243
+ const next = targets.filter((candidate) => candidate.id !== target.id);
1244
+ next.push(target);
1245
+ return next.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
1246
+ }
1247
+ function isMissingBrowserTarget(error) {
1248
+ return error instanceof OpenGeniApiError && error.code === "target_not_found";
1249
+ }
1250
+
1251
+ // src/hooks/use-browser-sessions.ts
1252
+ import { useCallback as useCallback6, useEffect as useEffect6, useMemo, useRef as useRef6, useState as useState6 } from "react";
1253
+ function useBrowserSessions(options = {}) {
1254
+ const {
1255
+ client,
1256
+ workspaceId,
1257
+ registerSessionReconciler,
1258
+ workspaceInteractionEvent,
1259
+ workspaceInteractionConnectionState
1260
+ } = useEmbeddedBrowserInteraction(options);
1261
+ const enabled = options.enabled ?? true;
1262
+ const [state, setState] = useState6(() => emptyState5(workspaceId, enabled));
1263
+ const requestRef = useRef6({
1264
+ id: 0,
1265
+ controller: null
1266
+ });
1267
+ const mountedRef = useRef6(true);
1268
+ const visibleState = state.workspaceId === workspaceId ? state : emptyState5(workspaceId, enabled);
1269
+ const cancelLoad = useCallback6(() => {
1270
+ const id = requestRef.current.id + 1;
1271
+ requestRef.current.controller?.abort();
1272
+ requestRef.current = { id, controller: null };
1273
+ }, []);
1274
+ const load = useCallback6(
1275
+ async (foreground) => {
1276
+ if (!enabled) return;
1277
+ const id = requestRef.current.id + 1;
1278
+ requestRef.current.controller?.abort();
1279
+ const controller = new AbortController();
1280
+ requestRef.current = { id, controller };
1281
+ setState((current) => ({
1282
+ ...current.workspaceId === workspaceId ? current : emptyState5(workspaceId, true),
1283
+ active: true,
1284
+ loading: foreground,
1285
+ refreshing: !foreground,
1286
+ error: foreground || current.workspaceId !== workspaceId ? null : current.error
1287
+ }));
1288
+ try {
1289
+ const response = await client.listBrowserSessions(workspaceId, {
1290
+ signal: controller.signal
1291
+ });
1292
+ if (!mountedRef.current || requestRef.current.id !== id) return;
1293
+ setState({
1294
+ workspaceId,
1295
+ active: true,
1296
+ revision: response.revision,
1297
+ sessions: sortBrowserSessions(response.sessions),
1298
+ loading: false,
1299
+ refreshing: false,
1300
+ error: null
1301
+ });
1302
+ } catch (cause) {
1303
+ if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id)
1304
+ return;
1305
+ const error = cause instanceof Error ? cause : new Error(String(cause));
1306
+ setState((current) => ({
1307
+ ...current.workspaceId === workspaceId ? current : emptyState5(workspaceId, true),
1308
+ loading: false,
1309
+ refreshing: false,
1310
+ error
1311
+ }));
1312
+ } finally {
1313
+ if (requestRef.current.id === id) {
1314
+ requestRef.current = { id, controller: null };
1315
+ }
1316
+ }
1317
+ },
1318
+ [client, enabled, workspaceId]
1319
+ );
1320
+ const refresh = useCallback6(async () => await load(false), [load]);
1321
+ useEffect6(() => {
1322
+ mountedRef.current = true;
1323
+ if (!enabled) {
1324
+ cancelLoad();
1325
+ setState(emptyState5(workspaceId, false));
1326
+ return;
1327
+ }
1328
+ void load(true);
1329
+ return () => {
1330
+ mountedRef.current = false;
1331
+ cancelLoad();
1332
+ };
1333
+ }, [cancelLoad, enabled, load, workspaceId]);
1334
+ useInteractionInvalidation({
1335
+ workspaceId,
1336
+ key: workspaceId,
1337
+ enabled,
1338
+ event: workspaceInteractionEvent,
1339
+ connectionState: workspaceInteractionConnectionState,
1340
+ knownRevision: visibleState.revision,
1341
+ refresh,
1342
+ fallbackPollIntervalMs: options.pollIntervalMs
1343
+ });
1344
+ useEffect6(() => {
1345
+ if (!enabled || !options.sessionId) return;
1346
+ return registerSessionReconciler(options.sessionId, `browser-sessions:${workspaceId}`, refresh);
1347
+ }, [enabled, options.sessionId, refresh, registerSessionReconciler, workspaceId]);
1348
+ const create = useCallback6(
1349
+ async (request) => {
1350
+ const response = await client.createBrowserSession(workspaceId, {
1351
+ ...request,
1352
+ operationId: request.operationId ?? crypto.randomUUID()
1353
+ });
1354
+ setState((current) => mergeSession(current, workspaceId, response.session));
1355
+ return response;
1356
+ },
1357
+ [client, workspaceId]
1358
+ );
1359
+ const end = useCallback6(
1360
+ async (browserSessionId, operationId = crypto.randomUUID()) => {
1361
+ const response = await client.endBrowserSession(workspaceId, browserSessionId, {
1362
+ operationId
1363
+ });
1364
+ setState((current) => mergeSession(current, workspaceId, response.session));
1365
+ return response;
1366
+ },
1367
+ [client, workspaceId]
1368
+ );
1369
+ const suspend = useCallback6(
1370
+ async (browserSessionId, operationId = crypto.randomUUID()) => {
1371
+ const response = await client.suspendBrowserSession(workspaceId, browserSessionId, {
1372
+ operationId
1373
+ });
1374
+ setState((current) => mergeSession(current, workspaceId, response.session));
1375
+ return response;
1376
+ },
1377
+ [client, workspaceId]
1378
+ );
1379
+ const resume = useCallback6(
1380
+ async (browserSessionId, operationId = crypto.randomUUID()) => {
1381
+ const response = await client.resumeBrowserSession(workspaceId, browserSessionId, {
1382
+ operationId
1383
+ });
1384
+ setState((current) => mergeSession(current, workspaceId, response.session));
1385
+ return response;
1386
+ },
1387
+ [client, workspaceId]
1388
+ );
1389
+ const relevantSessions = useMemo(
1390
+ () => options.sessionId ? visibleState.sessions.filter(
1391
+ (session) => session.associations.some((association) => association.sessionId === options.sessionId)
1392
+ ) : [],
1393
+ [options.sessionId, visibleState.sessions]
1394
+ );
1395
+ return {
1396
+ revision: visibleState.revision,
1397
+ sessions: visibleState.sessions,
1398
+ relevantSessions,
1399
+ // A disabled registry is deliberately empty. Expose the first re-enabled
1400
+ // render as loading so consumers do not reconcile against that empty gap.
1401
+ loading: enabled ? !visibleState.active || visibleState.loading : false,
1402
+ refreshing: visibleState.refreshing,
1403
+ error: visibleState.error,
1404
+ refresh,
1405
+ create,
1406
+ suspend,
1407
+ resume,
1408
+ end
1409
+ };
1410
+ }
1411
+ function emptyState5(workspaceId, loading) {
1412
+ return {
1413
+ workspaceId,
1414
+ active: false,
1415
+ revision: 0,
1416
+ sessions: [],
1417
+ loading,
1418
+ refreshing: false,
1419
+ error: null
1420
+ };
1421
+ }
1422
+ function sortBrowserSessions(sessions) {
1423
+ return [...sessions].sort((left, right) => {
1424
+ const live = Number(isLiveBrowser(right)) - Number(isLiveBrowser(left));
1425
+ return live || timestamp3(right.lastUsedAt) - timestamp3(left.lastUsedAt) || left.id.localeCompare(right.id);
1426
+ });
1427
+ }
1428
+ function isLiveBrowser(session) {
1429
+ return !["ending", "ended", "failed", "lost"].includes(session.lifecycle);
1430
+ }
1431
+ function timestamp3(value) {
1432
+ const parsed = Date.parse(value);
1433
+ return Number.isFinite(parsed) ? parsed : 0;
1434
+ }
1435
+ function mergeSession(current, workspaceId, session) {
1436
+ if (current.workspaceId !== workspaceId) return current;
1437
+ const sessions = current.sessions.filter((candidate) => candidate.id !== session.id);
1438
+ sessions.push(session);
1439
+ return { ...current, sessions: sortBrowserSessions(sessions), error: null };
1440
+ }
1441
+
1442
+ // src/hooks/use-site-auth-connections.ts
1443
+ import { useCallback as useCallback7, useEffect as useEffect7, useRef as useRef7, useState as useState7 } from "react";
1444
+ function useSiteAuthConnections(options = {}) {
1445
+ const { client, workspaceId, workspaceInteractionEvent, workspaceInteractionConnectionState } = useEmbeddedBrowserInteraction(options);
1446
+ const enabled = options.enabled ?? true;
1447
+ const includeArchived = options.includeArchived ?? false;
1448
+ const key = `${workspaceId}:${includeArchived ? "all" : "active"}`;
1449
+ const [state, setState] = useState7(() => emptyState6(key, enabled));
1450
+ const [mutationCount, setMutationCount] = useState7(0);
1451
+ const requestRef = useRef7({
1452
+ id: 0,
1453
+ controller: null
1454
+ });
1455
+ const mountedRef = useRef7(true);
1456
+ const visible = state.key === key ? state : emptyState6(key, enabled);
1457
+ const cancelLoad = useCallback7(() => {
1458
+ requestRef.current.controller?.abort();
1459
+ requestRef.current = { id: requestRef.current.id + 1, controller: null };
1460
+ }, []);
1461
+ const load = useCallback7(
1462
+ async (foreground) => {
1463
+ if (!enabled) return;
1464
+ const id = requestRef.current.id + 1;
1465
+ requestRef.current.controller?.abort();
1466
+ const controller = new AbortController();
1467
+ requestRef.current = { id, controller };
1468
+ setState((current) => ({
1469
+ ...current.key === key ? current : emptyState6(key, true),
1470
+ loading: foreground,
1471
+ refreshing: !foreground,
1472
+ error: foreground || current.key !== key ? null : current.error
1473
+ }));
1474
+ try {
1475
+ const response = await client.listSiteAuthConnections(workspaceId, {
1476
+ includeArchived,
1477
+ signal: controller.signal
1478
+ });
1479
+ if (!mountedRef.current || requestRef.current.id !== id) return;
1480
+ setState({
1481
+ key,
1482
+ revision: response.revision,
1483
+ connections: sortConnections(response.connections),
1484
+ loading: false,
1485
+ refreshing: false,
1486
+ error: null
1487
+ });
1488
+ } catch (cause) {
1489
+ if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id)
1490
+ return;
1491
+ setState((current) => ({
1492
+ ...current.key === key ? current : emptyState6(key, true),
1493
+ loading: false,
1494
+ refreshing: false,
1495
+ error: asError2(cause)
1496
+ }));
1497
+ } finally {
1498
+ if (requestRef.current.id === id) requestRef.current = { id, controller: null };
1499
+ }
1500
+ },
1501
+ [client, enabled, includeArchived, key, workspaceId]
1502
+ );
1503
+ const refresh = useCallback7(async () => await load(false), [load]);
1504
+ useEffect7(() => {
1505
+ mountedRef.current = true;
1506
+ if (!enabled) {
1507
+ cancelLoad();
1508
+ setState(emptyState6(key, false));
1509
+ return;
1510
+ }
1511
+ void load(true);
1512
+ return () => {
1513
+ mountedRef.current = false;
1514
+ cancelLoad();
1515
+ };
1516
+ }, [cancelLoad, enabled, key, load]);
1517
+ useInteractionInvalidation({
1518
+ workspaceId,
1519
+ key,
1520
+ enabled,
1521
+ event: workspaceInteractionEvent,
1522
+ connectionState: workspaceInteractionConnectionState,
1523
+ knownRevision: visible.revision,
1524
+ refresh
1525
+ });
1526
+ const mutate = useCallback7(async (operation) => {
1527
+ setMutationCount((count) => count + 1);
1528
+ try {
1529
+ return await operation();
1530
+ } finally {
1531
+ if (mountedRef.current) setMutationCount((count) => Math.max(0, count - 1));
1532
+ }
1533
+ }, []);
1534
+ const merge = useCallback7(
1535
+ (connection) => {
1536
+ setState((current) => {
1537
+ if (current.key !== key) return current;
1538
+ const connections = current.connections.filter(
1539
+ (candidate) => candidate.id !== connection.id
1540
+ );
1541
+ if (includeArchived || connection.status !== "archived") connections.push(connection);
1542
+ return { ...current, connections: sortConnections(connections), error: null };
1543
+ });
1544
+ },
1545
+ [includeArchived, key]
1546
+ );
1547
+ const create = useCallback7(
1548
+ async (request) => await mutate(async () => {
1549
+ const response = await client.createSiteAuthConnection(workspaceId, {
1550
+ ...request,
1551
+ operationId: request.operationId ?? crypto.randomUUID()
1552
+ });
1553
+ merge(response.connection);
1554
+ return response;
1555
+ }),
1556
+ [client, merge, mutate, workspaceId]
1557
+ );
1558
+ const update = useCallback7(
1559
+ async (connectionId, request) => await mutate(async () => {
1560
+ const response = await client.updateSiteAuthConnection(workspaceId, connectionId, {
1561
+ ...request,
1562
+ operationId: request.operationId ?? crypto.randomUUID()
1563
+ });
1564
+ merge(response.connection);
1565
+ return response;
1566
+ }),
1567
+ [client, merge, mutate, workspaceId]
1568
+ );
1569
+ return {
1570
+ revision: visible.revision,
1571
+ connections: visible.connections,
1572
+ loading: visible.loading,
1573
+ refreshing: visible.refreshing,
1574
+ mutating: mutationCount > 0,
1575
+ error: visible.error,
1576
+ refresh,
1577
+ create,
1578
+ update
1579
+ };
1580
+ }
1581
+ function emptyState6(key, loading) {
1582
+ return {
1583
+ key,
1584
+ revision: 0,
1585
+ connections: [],
1586
+ loading,
1587
+ refreshing: false,
1588
+ error: null
1589
+ };
1590
+ }
1591
+ function sortConnections(connections) {
1592
+ return [...connections].sort(
1593
+ (left, right) => Number(left.status === "archived") - Number(right.status === "archived") || left.name.localeCompare(right.name) || left.accountLabel.localeCompare(right.accountLabel)
1594
+ );
1595
+ }
1596
+ function asError2(cause) {
1597
+ return cause instanceof Error ? cause : new Error(String(cause));
1598
+ }
1599
+
1600
+ // src/components/browser-input.ts
1601
+ var HUMAN_BROWSER_HOME_URL = "https://www.google.com/";
1602
+ var HUMAN_BROWSER_SEARCH_URL = "https://www.google.com/search?q=";
1603
+ function normalizeBrowserAddress(value) {
1604
+ const trimmed = value.trim();
1605
+ if (!trimmed) return null;
1606
+ if (/^(?:https?|about):/iu.test(trimmed)) {
1607
+ try {
1608
+ const url = new URL(trimmed);
1609
+ return ["http:", "https:", "about:"].includes(url.protocol) ? url.href : null;
1610
+ } catch {
1611
+ return `${HUMAN_BROWSER_SEARCH_URL}${encodeURIComponent(trimmed)}`;
1612
+ }
1613
+ }
1614
+ const isLocal = /^(?:localhost|127(?:\.\d{1,3}){3}|\[::1\])(?::\d+)?(?:[/?#]|$)/iu.test(trimmed);
1615
+ const looksLikeHost = !/\s/u.test(trimmed) && (isLocal || /^(?:[^./\s]+\.)+[^./\s]+(?::\d+)?(?:[/?#]|$)/u.test(trimmed));
1616
+ if (!looksLikeHost) return `${HUMAN_BROWSER_SEARCH_URL}${encodeURIComponent(trimmed)}`;
1617
+ try {
1618
+ return new URL(`${isLocal ? "http" : "https"}://${trimmed}`).href;
1619
+ } catch {
1620
+ return `${HUMAN_BROWSER_SEARCH_URL}${encodeURIComponent(trimmed)}`;
1621
+ }
1622
+ }
1623
+ function browserKey(event, hostPlatform = interactionHostPlatform()) {
1624
+ if (["Alt", "AltGraph", "Control", "Meta", "Shift"].includes(event.key)) return null;
1625
+ const special = /* @__PURE__ */ new Set([
1626
+ "Enter",
1627
+ "Tab",
1628
+ "Escape",
1629
+ "Backspace",
1630
+ "Delete",
1631
+ "ArrowUp",
1632
+ "ArrowDown",
1633
+ "ArrowLeft",
1634
+ "ArrowRight"
1635
+ ]);
1636
+ const modified = event.altKey || event.ctrlKey || event.metaKey;
1637
+ if (!modified && !special.has(event.key)) return null;
1638
+ const parts = [];
1639
+ if (event.ctrlKey) parts.push(hostPlatform === "mac" ? "Control" : "Mod");
1640
+ if (event.altKey) parts.push("Alt");
1641
+ if (event.metaKey) parts.push(hostPlatform === "mac" ? "Mod" : "Meta");
1642
+ if (event.shiftKey) parts.push("Shift");
1643
+ parts.push(event.key === " " ? "Space" : event.key);
1644
+ return parts.join("+");
1645
+ }
1646
+
1647
+ // src/components/browser-viewer.tsx
1648
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
1649
+ function BrowserViewer({
1650
+ sessionId,
1651
+ enabled = true,
1652
+ className,
1653
+ onNotify,
1654
+ webSocketFactory,
1655
+ renderEmpty,
1656
+ createLinkedComputer,
1657
+ onOpenComputer,
1658
+ initialBrowserSessionId,
1659
+ onBrowserSessionIdChange,
1660
+ browserExtensionSetupUrl,
1661
+ ...override
1662
+ }) {
1663
+ const registry = useBrowserSessions({ ...override, sessionId, enabled });
1664
+ const attached = useAttachedBrowsers({ ...override, enabled });
1665
+ const onlineAttachedBridges = useMemo2(
1666
+ () => attached.bridges.filter((bridge) => bridge.state === "online"),
1667
+ [attached.bridges]
1668
+ );
1669
+ const profiles = useBrowserIdentities({ ...override, enabled, includeArchived: true });
1670
+ const siteAuth = useSiteAuthConnections({
1671
+ ...override,
1672
+ enabled: enabled && profiles.identities.length > 0
1673
+ });
1674
+ const createRegistryBrowser = registry.create;
1675
+ const refreshRegistry = registry.refresh;
1676
+ const loadProfileRevisions = profiles.revisions;
1677
+ const liveSessions = useMemo2(
1678
+ () => registry.sessions.filter((session) => isLiveBrowser2(session)),
1679
+ [registry.sessions]
1680
+ );
1681
+ const attachedGenerationLoss = useMemo2(
1682
+ () => attachedChromeGenerationLoss(registry.sessions),
1683
+ [registry.sessions]
1684
+ );
1685
+ const endedGenerationLossRef = useRef8(/* @__PURE__ */ new Set());
1686
+ const endStaleBrowser = registry.end;
1687
+ useEffect8(() => {
1688
+ if (!enabled) return;
1689
+ for (const session of registry.sessions) {
1690
+ if (session.lifecycle !== "lost" || session.failureCode !== "controller_transition_expired" || session.placement.kind !== "attached_device") {
1691
+ continue;
1692
+ }
1693
+ if (endedGenerationLossRef.current.has(session.id)) continue;
1694
+ endedGenerationLossRef.current.add(session.id);
1695
+ void endStaleBrowser(session.id).catch(() => {
1696
+ endedGenerationLossRef.current.delete(session.id);
1697
+ });
1698
+ }
1699
+ }, [enabled, endStaleBrowser, registry.sessions]);
1700
+ const replacementChromeDevice = useMemo2(() => {
1701
+ if (!attachedGenerationLoss) return null;
1702
+ return attached.devices.find((candidate) => candidate.id === attachedGenerationLoss.deviceId) ?? null;
1703
+ }, [attached.devices, attachedGenerationLoss]);
1704
+ const relevant = useMemo2(
1705
+ () => registry.relevantSessions.filter((session) => isLiveBrowser2(session)),
1706
+ [registry.relevantSessions]
1707
+ );
1708
+ const [selection, setSelection] = useState8(
1709
+ () => initialBrowserSessionId ? { sessionId: initialBrowserSessionId, pinned: true } : null
1710
+ );
1711
+ const selectBrowserSession = useCallback8(
1712
+ (next) => {
1713
+ setSelection(next);
1714
+ onBrowserSessionIdChange?.(next?.sessionId ?? null);
1715
+ },
1716
+ [onBrowserSessionIdChange]
1717
+ );
1718
+ const interventions = useInteractionInterventions({
1719
+ ...override,
1720
+ enabled,
1721
+ resourceKind: "browser_session"
1722
+ });
1723
+ const [creating, setCreating] = useState8(false);
1724
+ const [savingProfile, setSavingProfile] = useState8(false);
1725
+ const [baseRevisionOrdinal, setBaseRevisionOrdinal] = useState8(null);
1726
+ const [profileHistory, setProfileHistory] = useState8(null);
1727
+ const [resumeAttempt, setResumeAttempt] = useState8(null);
1728
+ const resumeAttemptsRef = useRef8(/* @__PURE__ */ new Map());
1729
+ const previousSessionIdRef = useRef8(sessionId);
1730
+ const seenInterventionIdsRef = useRef8(/* @__PURE__ */ new Set());
1731
+ const diagnosticRequestRef = useRef8(0);
1732
+ const [diagnosticsView, setDiagnosticsView] = useState8(null);
1733
+ const notifyError = useCallback8(
1734
+ (cause, fallback) => {
1735
+ const message = cause instanceof Error ? cause.message : fallback;
1736
+ onNotify?.({ kind: "error", message });
1737
+ },
1738
+ [onNotify]
1739
+ );
1740
+ useEffect8(() => {
1741
+ if (previousSessionIdRef.current !== sessionId) {
1742
+ previousSessionIdRef.current = sessionId;
1743
+ setSelection(
1744
+ initialBrowserSessionId ? { sessionId: initialBrowserSessionId, pinned: true } : null
1745
+ );
1746
+ }
1747
+ }, [initialBrowserSessionId, sessionId]);
1748
+ useEffect8(() => {
1749
+ if (!enabled || registry.loading) return;
1750
+ const selectedStillLive = liveSessions.some((session) => session.id === selection?.sessionId);
1751
+ if (selection?.pinned && selectedStillLive) return;
1752
+ const preferred = relevant[0] ?? null;
1753
+ if (!preferred) {
1754
+ if (selection) selectBrowserSession(null);
1755
+ return;
1756
+ }
1757
+ if (!selectedStillLive || !selection?.pinned) {
1758
+ if (selection?.sessionId !== preferred.id || selection.pinned) {
1759
+ selectBrowserSession({ sessionId: preferred.id, pinned: false });
1760
+ }
1761
+ }
1762
+ }, [enabled, liveSessions, registry.loading, relevant, selectBrowserSession, selection]);
1763
+ const selectedRegistrySession = useMemo2(
1764
+ () => liveSessions.find((session) => session.id === selection?.sessionId) ?? null,
1765
+ [liveSessions, selection?.sessionId]
1766
+ );
1767
+ const interventionCounts = useMemo2(
1768
+ () => countInterventions(interventions.interventions),
1769
+ [interventions.interventions]
1770
+ );
1771
+ const selectedInterventions = useMemo2(
1772
+ () => interventions.interventions.filter(
1773
+ (intervention) => intervention.resourceId === selection?.sessionId
1774
+ ),
1775
+ [interventions.interventions, selection?.sessionId]
1776
+ );
1777
+ const controllerReady = selectedRegistrySession?.lifecycle === "active";
1778
+ const resumeSession = registry.resume;
1779
+ const wakeBrowser = useCallback8(
1780
+ async (session, retry = false) => {
1781
+ const current = resumeAttemptsRef.current.get(session.id);
1782
+ if (current?.running || !retry && current) return;
1783
+ const operationId = current && !current.terminal ? current.operationId : crypto.randomUUID();
1784
+ const attempt = {
1785
+ operationId,
1786
+ running: true,
1787
+ terminal: false,
1788
+ error: null
1789
+ };
1790
+ resumeAttemptsRef.current.set(session.id, attempt);
1791
+ setResumeAttempt({ sessionId: session.id, attempt });
1792
+ try {
1793
+ const response = await resumeSession(session.id, operationId);
1794
+ if (response.operation.state !== "completed") {
1795
+ throw Object.assign(
1796
+ new Error(response.operation.error?.message ?? "Browser could not reopen."),
1797
+ { terminal: true }
1798
+ );
1799
+ }
1800
+ resumeAttemptsRef.current.delete(session.id);
1801
+ setResumeAttempt((visible) => visible?.sessionId === session.id ? null : visible);
1802
+ } catch (cause) {
1803
+ const error = cause instanceof Error ? cause : new Error(String(cause));
1804
+ const failed = {
1805
+ operationId,
1806
+ running: false,
1807
+ terminal: typeof cause === "object" && cause !== null && "terminal" in cause ? cause.terminal === true : false,
1808
+ error
1809
+ };
1810
+ resumeAttemptsRef.current.set(session.id, failed);
1811
+ setResumeAttempt({ sessionId: session.id, attempt: failed });
1812
+ notifyError(error, "Could not reopen this browser.");
1813
+ }
1814
+ },
1815
+ [notifyError, resumeSession]
1816
+ );
1817
+ useEffect8(() => {
1818
+ if (!selectedRegistrySession) return;
1819
+ if (selectedRegistrySession.lifecycle === "active") {
1820
+ resumeAttemptsRef.current.delete(selectedRegistrySession.id);
1821
+ setResumeAttempt(
1822
+ (visible) => visible?.sessionId === selectedRegistrySession.id ? null : visible
1823
+ );
1824
+ return;
1825
+ }
1826
+ if (selectedRegistrySession.lifecycle === "suspended") {
1827
+ void wakeBrowser(selectedRegistrySession);
1828
+ }
1829
+ }, [selectedRegistrySession, wakeBrowser]);
1830
+ const browser = useBrowserSession({
1831
+ ...override,
1832
+ browserSessionId: selection?.sessionId ?? null,
1833
+ enabled: enabled && selection !== null && controllerReady
1834
+ });
1835
+ useEffect8(() => {
1836
+ if (!isSourcePlacementChangedError(browser.error, "browser_session")) return;
1837
+ void refreshRegistry();
1838
+ }, [browser.error, refreshRegistry]);
1839
+ const downloads = useBrowserDownloads({
1840
+ ...override,
1841
+ browserSessionId: selection?.sessionId ?? null,
1842
+ enabled: enabled && diagnosticsView !== null && controllerReady && browser.session?.capabilities.downloads === true
1843
+ });
1844
+ const loadBrowserDiagnostics = browser.diagnostics;
1845
+ useEffect8(() => {
1846
+ for (const intervention of interventions.interventions) {
1847
+ if (seenInterventionIdsRef.current.has(intervention.id)) continue;
1848
+ seenInterventionIdsRef.current.add(intervention.id);
1849
+ onNotify?.({
1850
+ kind: "info",
1851
+ message: `${interventionTitle(intervention)}: ${intervention.reason}`
1852
+ });
1853
+ }
1854
+ }, [interventions.interventions, onNotify]);
1855
+ const resolveIntervention = useCallback8(
1856
+ (intervention, outcome) => {
1857
+ void interventions.resolve(intervention.id, {
1858
+ expectedVersion: intervention.version,
1859
+ outcome
1860
+ }).then(() => {
1861
+ onNotify?.({
1862
+ kind: "info",
1863
+ message: outcome === "completed" ? "Agent notified. Continuing work." : "Request cancelled."
1864
+ });
1865
+ }).catch((cause) => notifyError(cause, "Could not update the browser request."));
1866
+ },
1867
+ [interventions, notifyError, onNotify]
1868
+ );
1869
+ const frames = useBrowserFrameStream({
1870
+ ...override,
1871
+ browserSessionId: selection?.sessionId ?? null,
1872
+ targetId: browser.selectedTarget?.id ?? null,
1873
+ enabled: enabled && selection !== null && controllerReady && browser.selectedTarget !== null && browser.session?.capabilities.liveFrames === true,
1874
+ stream: { format: "jpeg", quality: 76, maxWidth: 1920, maxHeight: 1200 },
1875
+ ...webSocketFactory ? { webSocketFactory } : {}
1876
+ });
1877
+ const displayedFrame = frameMatchesSelectedTarget(
1878
+ frames.frame,
1879
+ browser.session,
1880
+ browser.selectedTarget
1881
+ ) ? frames.frame : null;
1882
+ const supportsLiveFrames = browser.session?.capabilities.liveFrames === true;
1883
+ const displayConnectionState = supportsLiveFrames ? frames.state === "live" && !displayedFrame ? "connecting" : frames.state : "semantic";
1884
+ const selectedProfile = useMemo2(
1885
+ () => profiles.identities.find(
1886
+ (identity) => identity.id === (browser.session?.identityId ?? selectedRegistrySession?.identityId)
1887
+ ) ?? null,
1888
+ [browser.session?.identityId, profiles.identities, selectedRegistrySession?.identityId]
1889
+ );
1890
+ const selectedProfileAuth = useMemo2(
1891
+ () => selectedProfile ? siteAuth.connections.filter(
1892
+ (connection) => connection.preferredIdentityId === selectedProfile.id
1893
+ ) : [],
1894
+ [selectedProfile, siteAuth.connections]
1895
+ );
1896
+ useEffect8(() => {
1897
+ diagnosticRequestRef.current += 1;
1898
+ setDiagnosticsView(null);
1899
+ }, [browser.session?.id, browser.selectedTarget?.id]);
1900
+ const loadDiagnostics = useCallback8(() => {
1901
+ const browserSessionId = browser.session?.id;
1902
+ const targetId = browser.selectedTarget?.id;
1903
+ if (!browserSessionId || !targetId) return;
1904
+ const requestId = diagnosticRequestRef.current + 1;
1905
+ diagnosticRequestRef.current = requestId;
1906
+ setDiagnosticsView((current) => ({
1907
+ browserSessionId,
1908
+ targetId,
1909
+ loading: true,
1910
+ batch: current?.browserSessionId === browserSessionId && current.targetId === targetId ? current.batch : null,
1911
+ error: null
1912
+ }));
1913
+ void loadBrowserDiagnostics({ limit: 100 }).then((batch) => {
1914
+ if (diagnosticRequestRef.current !== requestId) return;
1915
+ setDiagnosticsView({ browserSessionId, targetId, loading: false, batch, error: null });
1916
+ }).catch((cause) => {
1917
+ if (diagnosticRequestRef.current !== requestId) return;
1918
+ setDiagnosticsView({
1919
+ browserSessionId,
1920
+ targetId,
1921
+ loading: false,
1922
+ batch: null,
1923
+ error: cause instanceof Error ? cause : new Error(String(cause))
1924
+ });
1925
+ });
1926
+ }, [browser.selectedTarget?.id, browser.session?.id, loadBrowserDiagnostics]);
1927
+ const closeDiagnostics = useCallback8(() => {
1928
+ diagnosticRequestRef.current += 1;
1929
+ setDiagnosticsView(null);
1930
+ }, []);
1931
+ const refreshDiagnostics = useCallback8(() => {
1932
+ loadDiagnostics();
1933
+ if (browser.session?.capabilities.downloads) void downloads.refresh();
1934
+ }, [browser.session?.capabilities.downloads, downloads, loadDiagnostics]);
1935
+ const saveDownload = useCallback8(
1936
+ async (downloadId, destinationPath, overwrite) => {
1937
+ const response = await downloads.saveToWorkspace(downloadId, destinationPath, { overwrite });
1938
+ onNotify?.({
1939
+ kind: "info",
1940
+ message: `${response.download.filename} saved to ${response.destinationPath}.`
1941
+ });
1942
+ return response;
1943
+ },
1944
+ [downloads, onNotify]
1945
+ );
1946
+ useEffect8(() => {
1947
+ const identityId = browser.session?.identityId ?? selectedRegistrySession?.identityId;
1948
+ const revisionId = browser.session?.baseRevisionId ?? selectedRegistrySession?.baseRevisionId;
1949
+ if (!identityId) {
1950
+ setBaseRevisionOrdinal(null);
1951
+ setProfileHistory(null);
1952
+ return;
1953
+ }
1954
+ let disposed = false;
1955
+ setBaseRevisionOrdinal(null);
1956
+ setProfileHistory({ identityId, loading: true, revisions: [] });
1957
+ void loadProfileRevisions(identityId).then((response) => {
1958
+ if (!disposed) {
1959
+ setBaseRevisionOrdinal(
1960
+ revisionId ? response.revisions.find((revision) => revision.id === revisionId)?.ordinal ?? null : null
1961
+ );
1962
+ setProfileHistory({ identityId, loading: false, revisions: response.revisions });
1963
+ }
1964
+ }).catch(() => {
1965
+ if (!disposed) setProfileHistory({ identityId, loading: false, revisions: [] });
1966
+ });
1967
+ return () => {
1968
+ disposed = true;
1969
+ };
1970
+ }, [
1971
+ browser.session?.baseRevisionId,
1972
+ browser.session?.identityId,
1973
+ loadProfileRevisions,
1974
+ selectedRegistrySession?.baseRevisionId,
1975
+ selectedRegistrySession?.identityId
1976
+ ]);
1977
+ const createBrowser = useCallback8(
1978
+ (choice = { kind: "clean" }) => {
1979
+ if (creating) return;
1980
+ const identity = choice.kind === "profile" ? profiles.identities.find((candidate) => candidate.id === choice.identityId) : null;
1981
+ if (choice.kind === "profile" && (!identity || identity.status !== "active")) {
1982
+ onNotify?.({
1983
+ kind: "error",
1984
+ message: "This browser profile is no longer available. Refresh and choose another."
1985
+ });
1986
+ return;
1987
+ }
1988
+ const device = choice.kind === "attached" ? choice.device : null;
1989
+ const browserName = device?.profileLabel ?? device?.name ?? (identity ? `${identity.name} browser` : "Browser");
1990
+ setCreating(true);
1991
+ void (async () => {
1992
+ let linkedComputer = null;
1993
+ let computerViewUnavailable = false;
1994
+ if (createLinkedComputer) {
1995
+ try {
1996
+ linkedComputer = await createLinkedComputer(
1997
+ `${browserName} desktop`,
1998
+ device ? { kind: "attached_device", deviceId: device.id } : void 0
1999
+ );
2000
+ } catch {
2001
+ computerViewUnavailable = true;
2002
+ }
2003
+ }
2004
+ const response = await createRegistryBrowser({
2005
+ sessionId,
2006
+ name: browserName,
2007
+ // Human-created browsers are always ordinary visual Chromium/Chrome.
2008
+ // Lightpanda remains an agent-only optimization requested explicitly
2009
+ // through browser_open; if an agent creates one it is still visible in
2010
+ // the shared BrowserSession switcher.
2011
+ headless: false,
2012
+ initialUrl: HUMAN_BROWSER_HOME_URL,
2013
+ ...device ? {
2014
+ placement: {
2015
+ kind: "attached_device",
2016
+ deviceId: device.id
2017
+ }
2018
+ } : {},
2019
+ ...linkedComputer ? {
2020
+ linkedComputerSessionId: linkedComputer.id,
2021
+ placement: linkedComputer.placement
2022
+ } : {},
2023
+ ...identity ? {
2024
+ identityId: identity.id,
2025
+ ...choice.kind === "profile" && choice.baseRevisionId ? { baseRevisionId: choice.baseRevisionId } : {}
2026
+ } : {}
2027
+ });
2028
+ selectBrowserSession({ sessionId: response.session.id, pinned: true });
2029
+ if (computerViewUnavailable) {
2030
+ onNotify?.({
2031
+ kind: "info",
2032
+ message: "Browser opened. Desktop view is unavailable on this placement."
2033
+ });
2034
+ }
2035
+ })().catch((cause) => notifyError(cause, "Could not open a browser.")).finally(() => setCreating(false));
2036
+ },
2037
+ [
2038
+ createLinkedComputer,
2039
+ createRegistryBrowser,
2040
+ creating,
2041
+ notifyError,
2042
+ onNotify,
2043
+ selectBrowserSession,
2044
+ profiles.identities,
2045
+ sessionId
2046
+ ]
2047
+ );
2048
+ const saveProfileVersion = useCallback8(
2049
+ async (newProfileName) => {
2050
+ const session = browser.session;
2051
+ if (!session || savingProfile || !session.capabilities.identityPublication) return false;
2052
+ setSavingProfile(true);
2053
+ try {
2054
+ let identity = selectedProfile;
2055
+ if (!identity) {
2056
+ const name = newProfileName?.trim() ?? "";
2057
+ if (!name) return false;
2058
+ identity = profiles.identities.find(
2059
+ (candidate) => candidate.status === "active" && candidate.revisionCount === 0 && candidate.name.localeCompare(name, void 0, { sensitivity: "base" }) === 0
2060
+ ) ?? (await profiles.create({ name })).identity;
2061
+ }
2062
+ const response = await profiles.publish(session.id, {
2063
+ identityId: identity.id,
2064
+ expectedHeadGeneration: identity.headGeneration,
2065
+ advanceDefault: true
2066
+ });
2067
+ setBaseRevisionOrdinal(response.revision.ordinal);
2068
+ setProfileHistory((current) => ({
2069
+ identityId: response.identity.id,
2070
+ loading: false,
2071
+ revisions: current?.identityId === response.identity.id ? [
2072
+ ...current.revisions.filter((revision) => revision.id !== response.revision.id),
2073
+ response.revision
2074
+ ] : [response.revision]
2075
+ }));
2076
+ await Promise.all([browser.refresh(), registry.refresh()]);
2077
+ onNotify?.({
2078
+ kind: "info",
2079
+ message: response.outcome === "saved_as_default" ? `${response.identity.name} version ${response.revision.ordinal} saved for future browsers.` : `${response.identity.name} version ${response.revision.ordinal} saved. Another version remains the default.`
2080
+ });
2081
+ return true;
2082
+ } catch (cause) {
2083
+ notifyError(cause, "Could not save this browser profile.");
2084
+ return false;
2085
+ } finally {
2086
+ setSavingProfile(false);
2087
+ }
2088
+ },
2089
+ [browser, notifyError, onNotify, profiles, registry, savingProfile, selectedProfile]
2090
+ );
2091
+ const updateProfile = useCallback8(
2092
+ async (identity, update) => {
2093
+ if (savingProfile) return false;
2094
+ setSavingProfile(true);
2095
+ try {
2096
+ const response = await profiles.update(identity.id, {
2097
+ expectedVersion: identity.version,
2098
+ ...update
2099
+ });
2100
+ if (update.defaultRevisionId) {
2101
+ const ordinal = profileHistory?.revisions.find(
2102
+ (revision) => revision.id === update.defaultRevisionId
2103
+ )?.ordinal;
2104
+ onNotify?.({
2105
+ kind: "info",
2106
+ message: `${response.identity.name}${ordinal ? ` version ${ordinal}` : ""} will open by default in future browsers.`
2107
+ });
2108
+ } else if (update.status === "archived") {
2109
+ onNotify?.({
2110
+ kind: "info",
2111
+ message: `${response.identity.name} hidden from new browsers. Existing browsers are unchanged.`
2112
+ });
2113
+ } else if (update.status === "active") {
2114
+ onNotify?.({ kind: "info", message: `${response.identity.name} restored.` });
2115
+ }
2116
+ return true;
2117
+ } catch (cause) {
2118
+ notifyError(cause, "Could not update this browser profile.");
2119
+ await profiles.refresh().catch(() => void 0);
2120
+ return false;
2121
+ } finally {
2122
+ setSavingProfile(false);
2123
+ }
2124
+ },
2125
+ [notifyError, onNotify, profileHistory?.revisions, profiles, savingProfile]
2126
+ );
2127
+ if (!enabled) return null;
2128
+ if (registry.loading && liveSessions.length === 0) {
2129
+ return /* @__PURE__ */ jsx(
2130
+ BrowserNotice,
2131
+ {
2132
+ icon: /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "animate-spin" }),
2133
+ text: "Finding browsers\u2026",
2134
+ ...className ? { className } : {}
2135
+ }
2136
+ );
2137
+ }
2138
+ if (liveSessions.length === 0) {
2139
+ if (renderEmpty) return /* @__PURE__ */ jsx(Fragment, { children: renderEmpty(() => createBrowser(), creating) });
2140
+ return /* @__PURE__ */ jsx(
2141
+ "div",
2142
+ {
2143
+ className: cn("flex h-full min-h-0 items-center justify-center bg-og-bg p-6", className),
2144
+ children: /* @__PURE__ */ jsxs("div", { className: "max-w-sm text-center", children: [
2145
+ /* @__PURE__ */ jsx("span", { className: "mx-auto grid size-10 place-items-center rounded-og-md border border-og-border bg-og-surface-1 text-og-muted", children: /* @__PURE__ */ jsx(Globe2Icon, { className: "size-4.5" }) }),
2146
+ /* @__PURE__ */ jsx("p", { className: "mt-3 text-og-menu font-medium text-og-fg", children: attachedGenerationLoss ? "Chrome reconnected\u2014open a fresh browser/desktop." : "No browser open" }),
2147
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-og-control leading-5 text-og-muted", children: attachedGenerationLoss ? "This Chrome profile is live again, but the previous browser cannot move to the new connection. Use Browser \u2192 New browser \u2192 Connected Chrome." : "A browser appears here when this agent\u2014or another agent in the workspace\u2014opens one." }),
2148
+ /* @__PURE__ */ jsx(
2149
+ BrowserLaunchMenu,
2150
+ {
2151
+ attachedBridges: onlineAttachedBridges,
2152
+ attachedDevices: attached.devices,
2153
+ identities: profiles.identities,
2154
+ creating,
2155
+ mutatingProfile: savingProfile,
2156
+ onCreate: createBrowser,
2157
+ onRestore: (identity) => updateProfile(identity, { status: "active" }),
2158
+ setupUrl: browserExtensionSetupUrl,
2159
+ prominent: true
2160
+ }
2161
+ ),
2162
+ registry.error ? /* @__PURE__ */ jsx("p", { className: "mt-3 text-og-control text-og-status-error", children: registry.error.message }) : null
2163
+ ] })
2164
+ }
2165
+ );
2166
+ }
2167
+ return /* @__PURE__ */ jsxs(
2168
+ "div",
2169
+ {
2170
+ className: cn("relative flex h-full min-h-0 flex-col overflow-hidden bg-og-bg", className),
2171
+ children: [
2172
+ /* @__PURE__ */ jsx(
2173
+ BrowserToolbar,
2174
+ {
2175
+ sessions: liveSessions,
2176
+ relevantSessionIds: new Set(relevant.map((session) => session.id)),
2177
+ selectedSessionId: selection?.sessionId ?? null,
2178
+ identities: profiles.identities,
2179
+ attachedBridges: onlineAttachedBridges,
2180
+ attachedDevices: attached.devices,
2181
+ selectedProfile,
2182
+ baseRevisionOrdinal,
2183
+ profileHistory,
2184
+ authConnections: selectedProfileAuth,
2185
+ creating,
2186
+ savingProfile,
2187
+ refreshing: registry.refreshing || attached.refreshing,
2188
+ interventionCounts,
2189
+ onSelect: (browserSessionId) => selectBrowserSession({ sessionId: browserSessionId, pinned: true }),
2190
+ onFollow: () => {
2191
+ const preferred = relevant[0];
2192
+ if (preferred) selectBrowserSession({ sessionId: preferred.id, pinned: false });
2193
+ },
2194
+ onCreate: createBrowser,
2195
+ onSaveProfile: saveProfileVersion,
2196
+ onUpdateProfile: updateProfile,
2197
+ onOpenProfileVersion: (identityId, baseRevisionId) => createBrowser({ kind: "profile", identityId, baseRevisionId }),
2198
+ onRefresh: () => void Promise.all([registry.refresh(), attached.refresh()]),
2199
+ setupUrl: browserExtensionSetupUrl
2200
+ }
2201
+ ),
2202
+ /* @__PURE__ */ jsx(
2203
+ InteractionInterventionBanner,
2204
+ {
2205
+ interventions: selectedInterventions,
2206
+ activeTargetId: browser.selectedTarget?.id ?? null,
2207
+ mutating: interventions.mutating,
2208
+ onOpen: (intervention) => void browser.selectTarget(intervention.targetId).catch((cause) => notifyError(cause, "Could not open the requested browser tab.")),
2209
+ onResolve: resolveIntervention
2210
+ }
2211
+ ),
2212
+ !selectedRegistrySession ? /* @__PURE__ */ jsx(
2213
+ BrowserUnselectedPanel,
2214
+ {
2215
+ peerCount: liveSessions.length,
2216
+ creating,
2217
+ onCreate: () => createBrowser()
2218
+ }
2219
+ ) : !controllerReady ? /* @__PURE__ */ jsx(
2220
+ BrowserLifecyclePanel,
2221
+ {
2222
+ session: selectedRegistrySession,
2223
+ attempt: resumeAttempt?.sessionId === selectedRegistrySession.id ? resumeAttempt.attempt : null,
2224
+ onResume: () => void wakeBrowser(selectedRegistrySession, true)
2225
+ }
2226
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
2227
+ /* @__PURE__ */ jsx(
2228
+ BrowserTabs,
2229
+ {
2230
+ targets: browser.targets,
2231
+ selectedTargetId: browser.selectedTarget?.id ?? null,
2232
+ mutating: savingProfile,
2233
+ tabControl: browser.session?.capabilities.tabs === true,
2234
+ onSelect: (targetId) => void browser.selectTarget(targetId).catch((cause) => notifyError(cause, "Could not switch tabs.")),
2235
+ onClose: (targetId) => void browser.closeTarget(targetId).catch((cause) => notifyError(cause, "Could not close the tab.")),
2236
+ onOpen: () => void browser.openTarget(HUMAN_BROWSER_HOME_URL).catch((cause) => notifyError(cause, "Could not open a tab."))
2237
+ }
2238
+ ),
2239
+ /* @__PURE__ */ jsx(
2240
+ BrowserAddressBar,
2241
+ {
2242
+ target: browser.selectedTarget,
2243
+ loading: browser.loading || browser.mutating || savingProfile,
2244
+ onNavigate: (url) => void browser.act({ type: "navigate", url }).catch((cause) => notifyError(cause, "Could not navigate.")),
2245
+ onReload: () => {
2246
+ const url = browser.selectedTarget?.url;
2247
+ if (url)
2248
+ void browser.act({ type: "navigate", url }).catch((cause) => notifyError(cause, "Could not reload."));
2249
+ }
2250
+ }
2251
+ ),
2252
+ /* @__PURE__ */ jsx(
2253
+ BrowserViewport,
2254
+ {
2255
+ frame: displayedFrame,
2256
+ connectionState: displayConnectionState,
2257
+ supportsLiveFrames,
2258
+ connectionError: frames.error,
2259
+ observation: browser.observation,
2260
+ mutating: browser.mutating || savingProfile,
2261
+ activityLabel: savingProfile ? "Saving browser version\u2026" : void 0,
2262
+ clipboardEnabled: browser.session?.capabilities.clipboard === true,
2263
+ onAction: async (action, frame) => {
2264
+ const receipt = frame ? await browser.actFromFrame(action, frame) : await browser.act(action);
2265
+ if (receipt.state !== "completed") {
2266
+ throw new Error(receipt.error?.message ?? "Browser input did not complete.");
2267
+ }
2268
+ return receipt;
2269
+ },
2270
+ onReadClipboard: browser.readClipboard,
2271
+ onReconnect: attachedGenerationLoss || isAttachedChromeGenerationLossError(frames.error) ? () => {
2272
+ if (replacementChromeDevice) {
2273
+ createBrowser({ kind: "attached", device: replacementChromeDevice });
2274
+ }
2275
+ } : frames.reconnect,
2276
+ reconnectLabel: (attachedGenerationLoss || isAttachedChromeGenerationLossError(frames.error)) && replacementChromeDevice ? "Open a fresh Connected Chrome" : void 0,
2277
+ reconnectMessage: attachedGenerationLoss || isAttachedChromeGenerationLossError(frames.error) ? "Chrome reconnected\u2014open a fresh browser/desktop." : void 0,
2278
+ onError: (cause) => notifyError(cause, "Browser input failed.")
2279
+ }
2280
+ ),
2281
+ /* @__PURE__ */ jsx(
2282
+ BrowserStatusBar,
2283
+ {
2284
+ session: browser.session,
2285
+ profile: selectedProfile,
2286
+ target: browser.selectedTarget,
2287
+ observation: browser.observation,
2288
+ connectionState: displayConnectionState,
2289
+ refreshing: registry.refreshing,
2290
+ diagnosticsOpen: diagnosticsView !== null,
2291
+ onOpenComputer: browser.session?.linkedComputerSessionId && onOpenComputer ? () => {
2292
+ const computerSessionId = browser.session?.linkedComputerSessionId;
2293
+ if (computerSessionId) onOpenComputer(computerSessionId);
2294
+ } : void 0,
2295
+ onDiagnostics: diagnosticsView ? closeDiagnostics : loadDiagnostics
2296
+ }
2297
+ )
2298
+ ] }),
2299
+ diagnosticsView ? /* @__PURE__ */ jsx(
2300
+ BrowserDiagnosticsDrawer,
2301
+ {
2302
+ session: browser.session,
2303
+ profile: selectedProfile,
2304
+ target: browser.selectedTarget,
2305
+ observation: browser.observation,
2306
+ baseRevisionOrdinal,
2307
+ state: diagnosticsView,
2308
+ downloads: downloads.downloads,
2309
+ downloadsEnabled: browser.session?.capabilities.downloads === true,
2310
+ downloadsLoading: downloads.loading,
2311
+ downloadsRefreshing: downloads.refreshing,
2312
+ downloadsError: downloads.error,
2313
+ savingDownloadIds: downloads.savingDownloadIds,
2314
+ onSaveDownload: saveDownload,
2315
+ onRefreshDownloads: () => void downloads.refresh(),
2316
+ onRefresh: refreshDiagnostics,
2317
+ onClose: closeDiagnostics
2318
+ }
2319
+ ) : null
2320
+ ]
2321
+ }
2322
+ );
2323
+ }
2324
+ function BrowserLifecyclePanel(props) {
2325
+ const failed = props.attempt?.error ?? null;
2326
+ const label = failed ? "Browser could not reopen" : props.session.lifecycle === "suspending" ? "Saving browser state\u2026" : props.session.lifecycle === "restoring" || props.attempt?.running ? "Opening browser\u2026" : props.session.lifecycle === "starting" ? "Starting browser\u2026" : "Browser is sleeping";
2327
+ const detail = failed ? failed.message : props.session.lifecycle === "suspended" ? "Its private state is saved. Opening it creates a fresh, isolated browser process." : "The browser will reconnect here when it is ready.";
2328
+ return /* @__PURE__ */ jsx("div", { className: "grid min-h-0 flex-1 place-items-center bg-og-bg p-6 text-center", children: /* @__PURE__ */ jsxs("div", { className: "max-w-sm", children: [
2329
+ /* @__PURE__ */ jsx("span", { className: "mx-auto grid size-10 place-items-center rounded-og-md border border-og-border bg-og-surface-1 text-og-muted", children: failed ? /* @__PURE__ */ jsx(CircleAlertIcon, { className: "size-4.5 text-og-status-error" }) : /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-4.5 animate-spin" }) }),
2330
+ /* @__PURE__ */ jsx("p", { className: "mt-3 text-og-menu font-medium text-og-fg", children: label }),
2331
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-og-control leading-5 text-og-muted", children: detail }),
2332
+ failed || props.session.lifecycle === "suspended" && !props.attempt?.running ? /* @__PURE__ */ jsx(
2333
+ "button",
2334
+ {
2335
+ type: "button",
2336
+ onClick: props.onResume,
2337
+ className: "mt-4 inline-flex h-8 items-center 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",
2338
+ children: "Open browser"
2339
+ }
2340
+ ) : null
2341
+ ] }) });
2342
+ }
2343
+ function BrowserToolbar(props) {
2344
+ const detailsRef = useRef8(null);
2345
+ const selected = props.sessions.find((session) => session.id === props.selectedSessionId);
2346
+ const current = props.sessions.filter((session) => props.relevantSessionIds.has(session.id));
2347
+ const others = props.sessions.filter((session) => !props.relevantSessionIds.has(session.id));
2348
+ const choose = (id) => {
2349
+ props.onSelect(id);
2350
+ detailsRef.current?.removeAttribute("open");
2351
+ };
2352
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-10 shrink-0 items-center gap-1.5 border-b border-og-border bg-og-surface-0 px-2", children: [
2353
+ /* @__PURE__ */ jsxs("details", { ref: detailsRef, className: "relative min-w-0", children: [
2354
+ /* @__PURE__ */ jsxs("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", children: [
2355
+ /* @__PURE__ */ jsx(Globe2Icon, { className: "size-3.5 shrink-0 text-og-muted" }),
2356
+ /* @__PURE__ */ jsx("span", { className: "truncate font-medium", children: selected?.name ?? "Browser" }),
2357
+ (props.interventionCounts.get(selected?.id ?? "") ?? 0) > 0 ? /* @__PURE__ */ jsx("span", { className: "size-1.5 shrink-0 rounded-full bg-og-status-waiting" }) : null,
2358
+ /* @__PURE__ */ jsx(ChevronDownIcon, { className: "size-3 shrink-0 text-og-subtle" })
2359
+ ] }),
2360
+ /* @__PURE__ */ jsxs("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", children: [
2361
+ /* @__PURE__ */ jsx(
2362
+ BrowserSessionGroup,
2363
+ {
2364
+ label: "Current agent",
2365
+ sessions: current,
2366
+ identities: props.identities,
2367
+ selectedId: props.selectedSessionId,
2368
+ interventionCounts: props.interventionCounts,
2369
+ onSelect: choose
2370
+ }
2371
+ ),
2372
+ /* @__PURE__ */ jsx(
2373
+ BrowserSessionGroup,
2374
+ {
2375
+ label: "Workspace browsers",
2376
+ sessions: others,
2377
+ identities: props.identities,
2378
+ selectedId: props.selectedSessionId,
2379
+ interventionCounts: props.interventionCounts,
2380
+ onSelect: choose
2381
+ }
2382
+ ),
2383
+ /* @__PURE__ */ jsxs("div", { className: "mt-1 flex gap-1 border-t border-og-border pt-1", children: [
2384
+ current.length > 0 ? /* @__PURE__ */ jsx(MenuButton, { onClick: props.onFollow, children: "Follow agent" }) : null,
2385
+ /* @__PURE__ */ jsxs(MenuButton, { onClick: () => props.onCreate(), disabled: props.creating, children: [
2386
+ /* @__PURE__ */ jsx(PlusIcon, { className: "size-3.5" }),
2387
+ " New browser"
2388
+ ] })
2389
+ ] })
2390
+ ] })
2391
+ ] }),
2392
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-og-xs text-og-subtle", children: placementLabel(selected) }),
2393
+ /* @__PURE__ */ jsx(
2394
+ BrowserProfileMenu,
2395
+ {
2396
+ session: selected ?? null,
2397
+ identity: props.selectedProfile,
2398
+ baseRevisionOrdinal: props.baseRevisionOrdinal,
2399
+ history: props.profileHistory,
2400
+ authConnections: props.authConnections,
2401
+ saving: props.savingProfile,
2402
+ onSave: props.onSaveProfile,
2403
+ onUpdate: props.onUpdateProfile,
2404
+ onOpenVersion: props.onOpenProfileVersion
2405
+ }
2406
+ ),
2407
+ /* @__PURE__ */ jsx(
2408
+ "button",
2409
+ {
2410
+ type: "button",
2411
+ onClick: props.onRefresh,
2412
+ className: "grid size-7 place-items-center rounded-og-sm text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg",
2413
+ "aria-label": "Refresh browsers",
2414
+ children: /* @__PURE__ */ jsx(RefreshCwIcon, { className: cn("size-3.5", props.refreshing && "animate-spin") })
2415
+ }
2416
+ ),
2417
+ /* @__PURE__ */ jsx(
2418
+ BrowserLaunchMenu,
2419
+ {
2420
+ attachedBridges: props.attachedBridges,
2421
+ attachedDevices: props.attachedDevices,
2422
+ identities: props.identities,
2423
+ creating: props.creating,
2424
+ mutatingProfile: props.savingProfile,
2425
+ onCreate: props.onCreate,
2426
+ onRestore: (identity) => props.onUpdateProfile(identity, { status: "active" }),
2427
+ setupUrl: props.setupUrl
2428
+ }
2429
+ )
2430
+ ] });
2431
+ }
2432
+ function BrowserUnselectedPanel(props) {
2433
+ return /* @__PURE__ */ jsx("div", { className: "grid min-h-0 flex-1 place-items-center bg-og-bg p-6 text-center", children: /* @__PURE__ */ jsxs("div", { className: "max-w-sm", children: [
2434
+ /* @__PURE__ */ jsx("span", { className: "mx-auto grid size-10 place-items-center rounded-og-md border border-og-border bg-og-surface-1 text-og-muted", children: /* @__PURE__ */ jsx(Globe2Icon, { className: "size-4.5" }) }),
2435
+ /* @__PURE__ */ jsx("p", { className: "mt-3 text-og-menu font-medium text-og-fg", children: "No browser for this agent" }),
2436
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-og-control leading-5 text-og-muted", children: props.peerCount === 1 ? "One workspace browser is available from the browser menu." : `${props.peerCount} workspace browsers are available from the browser menu.` }),
2437
+ /* @__PURE__ */ jsxs(
2438
+ "button",
2439
+ {
2440
+ type: "button",
2441
+ disabled: props.creating,
2442
+ onClick: props.onCreate,
2443
+ 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",
2444
+ children: [
2445
+ /* @__PURE__ */ jsx(PlusIcon, { className: "size-3.5" }),
2446
+ props.creating ? "Opening\u2026" : "New browser"
2447
+ ]
2448
+ }
2449
+ )
2450
+ ] }) });
2451
+ }
2452
+ function BrowserSessionGroup(props) {
2453
+ if (props.sessions.length === 0) return null;
2454
+ return /* @__PURE__ */ jsxs("div", { className: "py-1", children: [
2455
+ /* @__PURE__ */ jsx("p", { className: "px-2 pb-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-subtle", children: props.label }),
2456
+ props.sessions.map((session) => {
2457
+ const identity = props.identities.find((candidate) => candidate.id === session.identityId);
2458
+ const interventionCount = props.interventionCounts.get(session.id) ?? 0;
2459
+ return /* @__PURE__ */ jsxs(
2460
+ "button",
2461
+ {
2462
+ type: "button",
2463
+ onClick: () => props.onSelect(session.id),
2464
+ className: cn(
2465
+ "flex w-full items-center gap-2 rounded-og-sm px-2 py-1.5 text-left transition hover:bg-og-surface-2",
2466
+ session.id === props.selectedId && "bg-og-surface-2"
2467
+ ),
2468
+ children: [
2469
+ /* @__PURE__ */ jsx(
2470
+ "span",
2471
+ {
2472
+ className: cn(
2473
+ "size-1.5 rounded-full",
2474
+ session.lifecycle === "active" ? "bg-og-status-running" : "bg-og-muted"
2475
+ )
2476
+ }
2477
+ ),
2478
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
2479
+ /* @__PURE__ */ jsx("span", { className: "block truncate text-og-control text-og-fg", children: session.name }),
2480
+ /* @__PURE__ */ jsxs("span", { className: "block truncate text-og-xs text-og-subtle", children: [
2481
+ identity ? `${identity.name} \xB7 ` : "",
2482
+ placementLabel(session)
2483
+ ] })
2484
+ ] }),
2485
+ interventionCount > 0 ? /* @__PURE__ */ jsx("span", { className: "rounded-full bg-og-status-waiting/12 px-1.5 py-0.5 text-[10px] font-medium text-og-status-waiting", children: interventionCount }) : null
2486
+ ]
2487
+ },
2488
+ session.id
2489
+ );
2490
+ })
2491
+ ] });
2492
+ }
2493
+ function BrowserLaunchMenu(props) {
2494
+ const detailsRef = useRef8(null);
2495
+ const choose = (choice) => {
2496
+ detailsRef.current?.removeAttribute("open");
2497
+ props.onCreate(choice);
2498
+ };
2499
+ const restore = async (identity) => {
2500
+ if (await props.onRestore(identity)) detailsRef.current?.removeAttribute("open");
2501
+ };
2502
+ const busy = props.creating || props.mutatingProfile;
2503
+ const activeIdentities = props.identities.filter((identity) => identity.status === "active");
2504
+ const archivedIdentities = props.identities.filter((identity) => identity.status === "archived");
2505
+ return /* @__PURE__ */ jsxs("details", { ref: detailsRef, className: cn("relative", props.prominent && "mt-4 inline-block"), children: [
2506
+ /* @__PURE__ */ jsxs(
2507
+ "summary",
2508
+ {
2509
+ onClick: (event) => {
2510
+ if (busy) event.preventDefault();
2511
+ },
2512
+ className: cn(
2513
+ "cursor-pointer list-none items-center justify-center gap-1.5 text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg [&::-webkit-details-marker]:hidden",
2514
+ props.prominent ? "inline-flex h-8 rounded-og-sm border border-og-border bg-og-surface-1 px-3 text-og-control font-medium text-og-fg" : "grid size-7 place-items-center rounded-og-sm",
2515
+ busy && "pointer-events-none opacity-50"
2516
+ ),
2517
+ "aria-label": "New browser",
2518
+ children: [
2519
+ busy ? /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-3.5 animate-spin" }) : /* @__PURE__ */ jsx(PlusIcon, { className: "size-3.5" }),
2520
+ props.prominent ? "New browser" : null,
2521
+ props.prominent ? /* @__PURE__ */ jsx(ChevronDownIcon, { className: "size-3" }) : null
2522
+ ]
2523
+ }
2524
+ ),
2525
+ /* @__PURE__ */ jsxs(
2526
+ "div",
2527
+ {
2528
+ className: cn(
2529
+ "absolute z-40 w-72 overflow-hidden rounded-og-md border border-og-border bg-og-surface-1 p-1 text-left shadow-xl",
2530
+ props.prominent ? "left-1/2 top-10 -translate-x-1/2" : "right-0 top-8"
2531
+ ),
2532
+ children: [
2533
+ props.attachedDevices.length > 0 || props.attachedBridges.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "pb-1", children: [
2534
+ /* @__PURE__ */ jsx("p", { className: "px-2 pb-1 pt-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-subtle", children: "Connected Chrome" }),
2535
+ props.attachedDevices.map((device) => /* @__PURE__ */ jsxs(
2536
+ "button",
2537
+ {
2538
+ type: "button",
2539
+ onClick: () => choose({ kind: "attached", device }),
2540
+ className: "flex w-full items-center gap-2 rounded-og-sm px-2 py-2 text-left transition hover:bg-og-surface-2",
2541
+ children: [
2542
+ /* @__PURE__ */ jsx(MonitorIcon, { className: "size-3.5 shrink-0 text-og-muted" }),
2543
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
2544
+ /* @__PURE__ */ jsx("span", { className: "block truncate text-og-control text-og-fg", children: device.profileLabel ?? device.name }),
2545
+ /* @__PURE__ */ jsxs("span", { className: "block truncate text-og-xs text-og-subtle", children: [
2546
+ device.browserName,
2547
+ " \xB7 ",
2548
+ device.tabCount,
2549
+ " open",
2550
+ " ",
2551
+ device.tabCount === 1 ? "tab" : "tabs"
2552
+ ] })
2553
+ ] }),
2554
+ /* @__PURE__ */ jsx("span", { className: "size-1.5 shrink-0 rounded-full bg-og-status-running" })
2555
+ ]
2556
+ },
2557
+ device.id
2558
+ )),
2559
+ props.attachedBridges.length > 0 ? props.setupUrl ? /* @__PURE__ */ jsxs(
2560
+ "a",
2561
+ {
2562
+ href: props.setupUrl,
2563
+ target: "_blank",
2564
+ rel: "noreferrer",
2565
+ onClick: () => detailsRef.current?.removeAttribute("open"),
2566
+ className: "flex w-full items-center gap-2 rounded-og-sm px-2 py-2 text-left transition hover:bg-og-surface-2",
2567
+ children: [
2568
+ /* @__PURE__ */ jsx(PlusIcon, { className: "size-3.5 shrink-0 text-og-muted" }),
2569
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
2570
+ /* @__PURE__ */ jsx("span", { className: "block truncate text-og-control text-og-fg", children: props.attachedDevices.length > 0 ? "Connect another Chrome profile" : "Connect this Chrome profile" }),
2571
+ /* @__PURE__ */ jsx("span", { className: "block truncate text-og-xs text-og-subtle", children: "Machine connected \xB7 Chrome extension missing" })
2572
+ ] })
2573
+ ]
2574
+ }
2575
+ ) : /* @__PURE__ */ jsxs("div", { className: "flex w-full items-center gap-2 px-2 py-2 text-left", children: [
2576
+ /* @__PURE__ */ jsx(MonitorIcon, { className: "size-3.5 shrink-0 text-og-muted" }),
2577
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
2578
+ /* @__PURE__ */ jsx("span", { className: "block truncate text-og-control text-og-fg", children: "Chrome connection ready" }),
2579
+ /* @__PURE__ */ jsx("span", { className: "block text-og-xs leading-4 text-og-subtle", children: "Install the OpenGeni Browser extension in the profile you want to use." })
2580
+ ] })
2581
+ ] }) : null
2582
+ ] }) : null,
2583
+ /* @__PURE__ */ jsx(
2584
+ "p",
2585
+ {
2586
+ className: cn(
2587
+ "px-2 pb-1 pt-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-subtle",
2588
+ (props.attachedDevices.length > 0 || props.attachedBridges.length > 0) && "border-t border-og-border"
2589
+ ),
2590
+ children: "Open isolated browser"
2591
+ }
2592
+ ),
2593
+ /* @__PURE__ */ jsxs(
2594
+ "button",
2595
+ {
2596
+ type: "button",
2597
+ onClick: () => choose({ kind: "clean" }),
2598
+ className: "flex w-full items-center gap-2 rounded-og-sm px-2 py-2 text-left transition hover:bg-og-surface-2",
2599
+ children: [
2600
+ /* @__PURE__ */ jsx(Globe2Icon, { className: "size-3.5 text-og-muted" }),
2601
+ /* @__PURE__ */ jsxs("span", { children: [
2602
+ /* @__PURE__ */ jsx("span", { className: "block text-og-control text-og-fg", children: "Fresh browser" }),
2603
+ /* @__PURE__ */ jsx("span", { className: "block text-og-xs text-og-subtle", children: "Visual browser \xB7 no saved state" })
2604
+ ] })
2605
+ ]
2606
+ }
2607
+ ),
2608
+ activeIdentities.length > 0 ? /* @__PURE__ */ jsx("div", { className: "mt-1 border-t border-og-border pt-1", children: activeIdentities.map((identity) => /* @__PURE__ */ jsxs(
2609
+ "button",
2610
+ {
2611
+ type: "button",
2612
+ onClick: () => choose({ kind: "profile", identityId: identity.id }),
2613
+ className: "flex w-full items-center gap-2 rounded-og-sm px-2 py-2 text-left transition hover:bg-og-surface-2",
2614
+ children: [
2615
+ /* @__PURE__ */ jsx(UserRoundIcon, { className: "size-3.5 text-og-muted" }),
2616
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0", children: [
2617
+ /* @__PURE__ */ jsx("span", { className: "block truncate text-og-control text-og-fg", children: identity.name }),
2618
+ /* @__PURE__ */ jsx("span", { className: "block text-og-xs text-og-subtle", children: identity.defaultRevisionId ? `${identity.revisionCount} saved version${identity.revisionCount === 1 ? "" : "s"}` : "Ready for first sign-in" })
2619
+ ] })
2620
+ ]
2621
+ },
2622
+ identity.id
2623
+ )) }) : null,
2624
+ archivedIdentities.length > 0 ? /* @__PURE__ */ jsxs("details", { className: "mt-1 border-t border-og-border pt-1", children: [
2625
+ /* @__PURE__ */ jsxs("summary", { className: "flex cursor-pointer list-none items-center gap-1.5 rounded-og-sm px-2 py-1.5 text-og-xs text-og-subtle transition hover:bg-og-surface-2 hover:text-og-fg [&::-webkit-details-marker]:hidden", children: [
2626
+ /* @__PURE__ */ jsx(ArchiveIcon, { className: "size-3" }),
2627
+ " Hidden profiles \xB7 ",
2628
+ archivedIdentities.length,
2629
+ /* @__PURE__ */ jsx(ChevronDownIcon, { className: "ml-auto size-3" })
2630
+ ] }),
2631
+ archivedIdentities.map((identity) => /* @__PURE__ */ jsxs(
2632
+ "button",
2633
+ {
2634
+ type: "button",
2635
+ disabled: busy,
2636
+ onClick: () => void restore(identity),
2637
+ className: "flex w-full items-center gap-2 rounded-og-sm px-2 py-2 text-left transition hover:bg-og-surface-2 disabled:opacity-50",
2638
+ children: [
2639
+ /* @__PURE__ */ jsx(RotateCcwIcon, { className: "size-3.5 text-og-muted" }),
2640
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1 truncate text-og-control text-og-fg", children: [
2641
+ "Restore ",
2642
+ identity.name
2643
+ ] })
2644
+ ]
2645
+ },
2646
+ identity.id
2647
+ ))
2648
+ ] }) : null
2649
+ ]
2650
+ }
2651
+ )
2652
+ ] });
2653
+ }
2654
+ function BrowserProfileMenu(props) {
2655
+ const detailsRef = useRef8(null);
2656
+ const nameInputId = useId();
2657
+ const [name, setName] = useState8("");
2658
+ const attached = props.session?.placement.kind === "attached_device";
2659
+ const canSave = (props.session?.capabilities.identityPublication ?? false) && props.identity?.status !== "archived";
2660
+ const revisions = props.identity && props.history?.identityId === props.identity.id ? [...props.history.revisions].sort((left, right) => right.ordinal - left.ordinal) : [];
2661
+ const save = async (newProfileName) => {
2662
+ if (await props.onSave(newProfileName)) {
2663
+ setName("");
2664
+ detailsRef.current?.removeAttribute("open");
2665
+ }
2666
+ };
2667
+ const openVersion = (identityId, baseRevisionId) => {
2668
+ detailsRef.current?.removeAttribute("open");
2669
+ props.onOpenVersion(identityId, baseRevisionId);
2670
+ };
2671
+ const versionLabel = attached ? "live" : props.identity ? props.baseRevisionOrdinal ? `v${props.baseRevisionOrdinal}` : props.session?.baseRevisionId ? "saved" : "unsaved" : "temporary";
2672
+ return /* @__PURE__ */ jsxs("details", { ref: detailsRef, className: "relative min-w-0", children: [
2673
+ /* @__PURE__ */ jsxs("summary", { className: "flex h-7 max-w-40 cursor-pointer list-none items-center gap-1.5 rounded-og-sm px-2 text-og-xs text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg [&::-webkit-details-marker]:hidden", children: [
2674
+ props.saving ? /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-3 animate-spin" }) : attached ? /* @__PURE__ */ jsx(MonitorIcon, { className: "size-3" }) : /* @__PURE__ */ jsx(UserRoundIcon, { className: "size-3" }),
2675
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: attached ? props.session?.name : props.identity?.name ?? "Temporary" }),
2676
+ attached || props.identity ? /* @__PURE__ */ jsxs("span", { className: "shrink-0 text-og-subtle", children: [
2677
+ "\xB7 ",
2678
+ versionLabel
2679
+ ] }) : null,
2680
+ /* @__PURE__ */ jsx(ChevronDownIcon, { className: "size-3 shrink-0 text-og-subtle" })
2681
+ ] }),
2682
+ /* @__PURE__ */ jsxs("div", { className: "absolute right-0 top-8 z-40 w-72 rounded-og-md border border-og-border bg-og-surface-1 p-3 shadow-xl", children: [
2683
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
2684
+ /* @__PURE__ */ jsx("span", { className: "grid size-7 shrink-0 place-items-center rounded-og-sm bg-og-surface-2 text-og-muted", children: attached ? /* @__PURE__ */ jsx(MonitorIcon, { className: "size-3.5" }) : /* @__PURE__ */ jsx(UserRoundIcon, { className: "size-3.5" }) }),
2685
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
2686
+ /* @__PURE__ */ jsx("p", { className: "truncate text-og-control font-medium text-og-fg", children: attached ? props.session?.name : props.identity?.name ?? "Temporary browser" }),
2687
+ /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-og-xs leading-4 text-og-subtle", children: attached ? "This session drives the existing Chrome profile and its current login state." : props.identity?.status === "archived" ? "Hidden from new browsers. This already-open browser is unchanged." : props.identity ? props.baseRevisionOrdinal ? `Started from version ${props.baseRevisionOrdinal}.` : "Not saved yet." : "This browser is not reusable yet." })
2688
+ ] })
2689
+ ] }),
2690
+ props.identity && revisions.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "mt-3 border-t border-og-border pt-2", children: [
2691
+ /* @__PURE__ */ jsx("p", { className: "px-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-subtle", children: "Saved versions" }),
2692
+ /* @__PURE__ */ jsx("div", { className: "mt-1 max-h-40 overflow-y-auto", children: revisions.map((revision) => {
2693
+ const isDefault = revision.id === props.identity?.defaultRevisionId;
2694
+ const isCurrent = revision.ordinal === props.baseRevisionOrdinal;
2695
+ const materialization = revisionMaterializationSummary(revision);
2696
+ return /* @__PURE__ */ jsxs(
2697
+ "div",
2698
+ {
2699
+ className: "flex w-full items-center gap-1 rounded-og-sm transition hover:bg-og-surface-2",
2700
+ children: [
2701
+ /* @__PURE__ */ jsxs(
2702
+ "button",
2703
+ {
2704
+ type: "button",
2705
+ disabled: props.saving || props.identity?.status === "archived",
2706
+ "aria-label": `Open ${props.identity.name} version ${revision.ordinal}`,
2707
+ onClick: () => openVersion(props.identity.id, revision.id),
2708
+ className: "flex min-w-0 flex-1 items-center gap-2 px-2 py-1.5 text-left disabled:opacity-50",
2709
+ children: [
2710
+ /* @__PURE__ */ jsx("span", { className: "grid size-4 place-items-center text-og-muted", children: isCurrent ? /* @__PURE__ */ jsx(CheckIcon, { className: "size-3.5 text-og-accent" }) : null }),
2711
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
2712
+ /* @__PURE__ */ jsxs("span", { className: "block text-og-control text-og-fg", children: [
2713
+ "Version ",
2714
+ revision.ordinal
2715
+ ] }),
2716
+ /* @__PURE__ */ jsx("span", { className: "block truncate text-[10px] text-og-subtle", children: materialization.label })
2717
+ ] }),
2718
+ isCurrent ? /* @__PURE__ */ jsx("span", { className: "text-[10px] text-og-subtle", children: "Current" }) : null
2719
+ ]
2720
+ }
2721
+ ),
2722
+ /* @__PURE__ */ jsx(
2723
+ "button",
2724
+ {
2725
+ type: "button",
2726
+ disabled: isDefault || props.saving || props.identity?.status === "archived",
2727
+ "aria-label": isDefault ? `${props.identity.name} version ${revision.ordinal} is the default` : `Use ${props.identity.name} version ${revision.ordinal} by default`,
2728
+ title: isDefault ? "Default version" : "Use by default",
2729
+ onClick: () => void props.onUpdate(props.identity, { defaultRevisionId: revision.id }),
2730
+ className: "mr-1 shrink-0 rounded-og-sm px-2 py-1 text-[10px] text-og-subtle transition hover:bg-og-surface-3 hover:text-og-fg disabled:cursor-default disabled:opacity-70",
2731
+ children: isDefault ? "Default" : "Make default"
2732
+ }
2733
+ )
2734
+ ]
2735
+ },
2736
+ revision.id
2737
+ );
2738
+ }) }),
2739
+ /* @__PURE__ */ jsx("p", { className: "mt-1 px-2 text-[10px] leading-4 text-og-subtle", children: "Changing the default affects only browsers opened later. Saved browser data can be copied; a website may still expire or re-verify its own session." })
2740
+ ] }) : props.identity && props.history?.identityId === props.identity.id && props.history.loading ? /* @__PURE__ */ jsxs("p", { className: "mt-3 flex items-center gap-1.5 border-t border-og-border px-2 pt-2 text-og-xs text-og-subtle", children: [
2741
+ /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-3 animate-spin" }),
2742
+ " Loading saved versions\u2026"
2743
+ ] }) : null,
2744
+ props.identity && props.authConnections.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "mt-3 border-t border-og-border pt-2", children: [
2745
+ /* @__PURE__ */ jsx("p", { className: "px-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-subtle", children: "Login health" }),
2746
+ /* @__PURE__ */ jsx("div", { className: "mt-1 space-y-1", children: props.authConnections.map((connection) => {
2747
+ const health = siteAuthHealth(connection);
2748
+ return /* @__PURE__ */ jsxs(
2749
+ "div",
2750
+ {
2751
+ className: "flex items-start gap-2 rounded-og-sm px-2 py-1.5",
2752
+ children: [
2753
+ /* @__PURE__ */ jsx("span", { className: cn("mt-1 size-1.5 shrink-0 rounded-full", health.dotClass) }),
2754
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
2755
+ /* @__PURE__ */ jsx("span", { className: "block truncate text-og-control text-og-fg", children: connection.name }),
2756
+ /* @__PURE__ */ jsxs("span", { className: "block truncate text-[10px] text-og-subtle", children: [
2757
+ connection.accountLabel,
2758
+ " \xB7 ",
2759
+ health.label
2760
+ ] })
2761
+ ] })
2762
+ ]
2763
+ },
2764
+ connection.id
2765
+ );
2766
+ }) })
2767
+ ] }) : null,
2768
+ canSave ? props.identity ? /* @__PURE__ */ jsxs(
2769
+ "button",
2770
+ {
2771
+ type: "button",
2772
+ onClick: () => void save(),
2773
+ disabled: props.saving,
2774
+ className: "mt-3 flex h-8 w-full items-center justify-center gap-1.5 rounded-og-sm border border-og-border bg-og-surface-2 px-3 text-og-control font-medium text-og-fg transition hover:bg-og-surface-3 disabled:opacity-50",
2775
+ children: [
2776
+ props.saving ? /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-3.5 animate-spin" }) : /* @__PURE__ */ jsx(SaveIcon, { className: "size-3.5" }),
2777
+ props.saving ? "Saving browser version\u2026" : "Save new version"
2778
+ ]
2779
+ }
2780
+ ) : /* @__PURE__ */ jsxs(
2781
+ "form",
2782
+ {
2783
+ className: "mt-3",
2784
+ onSubmit: (event) => {
2785
+ event.preventDefault();
2786
+ if (name.trim()) void save(name);
2787
+ },
2788
+ children: [
2789
+ /* @__PURE__ */ jsx("label", { className: "text-og-xs font-medium text-og-muted", htmlFor: nameInputId, children: "Profile name" }),
2790
+ /* @__PURE__ */ jsxs("div", { className: "mt-1 flex gap-1.5", children: [
2791
+ /* @__PURE__ */ jsx(
2792
+ "input",
2793
+ {
2794
+ id: nameInputId,
2795
+ value: name,
2796
+ onInput: (event) => setName(event.currentTarget.value),
2797
+ maxLength: 200,
2798
+ placeholder: "Work",
2799
+ className: "h-8 min-w-0 flex-1 rounded-og-sm border border-og-border bg-og-bg px-2 text-og-control text-og-fg placeholder:text-og-subtle focus:border-og-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent"
2800
+ }
2801
+ ),
2802
+ /* @__PURE__ */ jsxs(
2803
+ "button",
2804
+ {
2805
+ type: "submit",
2806
+ disabled: !name.trim() || props.saving,
2807
+ className: "inline-flex h-8 items-center gap-1 rounded-og-sm bg-og-accent px-2.5 text-og-control font-medium text-white transition hover:opacity-90 disabled:opacity-40",
2808
+ children: [
2809
+ props.saving ? /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-3.5 animate-spin" }) : /* @__PURE__ */ jsx(SaveIcon, { className: "size-3.5" }),
2810
+ "Save"
2811
+ ]
2812
+ }
2813
+ )
2814
+ ] })
2815
+ ]
2816
+ }
2817
+ ) : /* @__PURE__ */ jsx("p", { className: "mt-3 rounded-og-sm bg-og-surface-2 px-2.5 py-2 text-og-xs text-og-subtle", children: attached ? "Chrome keeps this profile's state directly; OpenGeni does not copy it automatically." : props.identity?.status === "archived" ? "Restore this profile before saving another version." : "This browser cannot save reusable profile state." }),
2818
+ !attached && props.identity ? /* @__PURE__ */ jsxs(
2819
+ "button",
2820
+ {
2821
+ type: "button",
2822
+ disabled: props.saving,
2823
+ onClick: () => void props.onUpdate(props.identity, {
2824
+ status: props.identity.status === "active" ? "archived" : "active"
2825
+ }),
2826
+ className: "mt-2 flex h-8 w-full items-center justify-center gap-1.5 rounded-og-sm px-3 text-og-control text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg disabled:opacity-50",
2827
+ children: [
2828
+ props.identity.status === "active" ? /* @__PURE__ */ jsx(ArchiveIcon, { className: "size-3.5" }) : /* @__PURE__ */ jsx(RotateCcwIcon, { className: "size-3.5" }),
2829
+ props.identity.status === "active" ? "Hide from new browsers" : "Restore profile"
2830
+ ]
2831
+ }
2832
+ ) : null,
2833
+ canSave ? /* @__PURE__ */ jsx("p", { className: "mt-2 text-[10px] leading-4 text-og-subtle", children: "Saving briefly restarts this browser. Other open browsers are unchanged." }) : null
2834
+ ] })
2835
+ ] });
2836
+ }
2837
+ function revisionMaterializationSummary(revision) {
2838
+ const materializations = revision.components.map((component) => component.materialization);
2839
+ const placementBound = materializations.find(
2840
+ (materialization) => materialization.portability === "placement_bound"
2841
+ );
2842
+ if (placementBound) {
2843
+ return {
2844
+ label: placementBound.reason ? `This machine only \xB7 ${placementBound.reason}` : "This machine only"
2845
+ };
2846
+ }
2847
+ const providerBound = materializations.find(
2848
+ (materialization) => materialization.portability === "provider_bound"
2849
+ );
2850
+ if (providerBound) {
2851
+ return {
2852
+ label: providerBound.reason ? `Provider-bound \xB7 ${providerBound.reason}` : "Provider-bound"
2853
+ };
2854
+ }
2855
+ return { label: "Portable browser data" };
2856
+ }
2857
+ function siteAuthHealth(connection) {
2858
+ switch (connection.verificationState) {
2859
+ case "verified":
2860
+ return { label: "Verified", dotClass: "bg-og-status-running" };
2861
+ case "needs_repair":
2862
+ return { label: "Sign-in needs attention", dotClass: "bg-og-status-waiting" };
2863
+ case "failed":
2864
+ return { label: "Verification failed", dotClass: "bg-og-status-error" };
2865
+ case "unknown":
2866
+ return { label: "Not checked yet", dotClass: "bg-og-muted" };
2867
+ }
2868
+ }
2869
+ function MenuButton(props) {
2870
+ return /* @__PURE__ */ jsx(
2871
+ "button",
2872
+ {
2873
+ type: "button",
2874
+ onClick: props.onClick,
2875
+ disabled: props.disabled,
2876
+ 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",
2877
+ children: props.children
2878
+ }
2879
+ );
2880
+ }
2881
+ function BrowserTabs(props) {
2882
+ const pages = props.targets.filter((target) => target.kind === "page" || target.kind === "popup");
2883
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-9 shrink-0 items-end gap-px overflow-x-auto border-b border-og-border bg-og-surface-0 px-1 pt-1", children: [
2884
+ pages.map((target) => /* @__PURE__ */ jsxs(
2885
+ "div",
2886
+ {
2887
+ className: cn(
2888
+ "group flex h-8 min-w-24 max-w-48 items-center rounded-t-og-sm border border-b-0 border-transparent px-2",
2889
+ target.id === props.selectedTargetId ? "border-og-border bg-og-bg text-og-fg" : "text-og-muted hover:bg-og-surface-1"
2890
+ ),
2891
+ children: [
2892
+ /* @__PURE__ */ jsx(
2893
+ "button",
2894
+ {
2895
+ type: "button",
2896
+ onClick: () => props.onSelect(target.id),
2897
+ className: "min-w-0 flex-1 truncate text-left text-og-control",
2898
+ children: target.title || shortUrl(target.url)
2899
+ }
2900
+ ),
2901
+ props.tabControl ? /* @__PURE__ */ jsx(
2902
+ "button",
2903
+ {
2904
+ type: "button",
2905
+ onClick: () => props.onClose(target.id),
2906
+ disabled: props.mutating,
2907
+ className: "ml-1 grid size-4 shrink-0 place-items-center rounded opacity-0 transition hover:bg-og-surface-3 group-hover:opacity-100 focus:opacity-100 disabled:opacity-30",
2908
+ "aria-label": `Close ${target.title || "tab"}`,
2909
+ children: /* @__PURE__ */ jsx(XIcon, { className: "size-3" })
2910
+ }
2911
+ ) : null
2912
+ ]
2913
+ },
2914
+ target.id
2915
+ )),
2916
+ props.tabControl ? /* @__PURE__ */ jsx(
2917
+ "button",
2918
+ {
2919
+ type: "button",
2920
+ onClick: props.onOpen,
2921
+ disabled: props.mutating,
2922
+ className: "mb-1 grid size-6 shrink-0 place-items-center rounded-og-sm text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg disabled:opacity-40",
2923
+ "aria-label": "New tab",
2924
+ children: /* @__PURE__ */ jsx(PlusIcon, { className: "size-3.5" })
2925
+ }
2926
+ ) : null
2927
+ ] });
2928
+ }
2929
+ function BrowserAddressBar(props) {
2930
+ const [draft, setDraft] = useState8(props.target?.url ?? "");
2931
+ const focusedRef = useRef8(false);
2932
+ useEffect8(() => {
2933
+ if (!focusedRef.current) setDraft(props.target?.url ?? "");
2934
+ }, [props.target?.id, props.target?.url]);
2935
+ const submit = (event) => {
2936
+ event.preventDefault();
2937
+ const normalized = normalizeBrowserAddress(draft);
2938
+ if (normalized) props.onNavigate(normalized);
2939
+ };
2940
+ return /* @__PURE__ */ jsxs(
2941
+ "form",
2942
+ {
2943
+ onSubmit: submit,
2944
+ className: "flex h-10 shrink-0 items-center gap-1.5 border-b border-og-border bg-og-bg px-2",
2945
+ children: [
2946
+ /* @__PURE__ */ jsx(
2947
+ "button",
2948
+ {
2949
+ type: "button",
2950
+ onClick: props.onReload,
2951
+ disabled: !props.target || props.loading,
2952
+ 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-35",
2953
+ "aria-label": "Reload",
2954
+ children: /* @__PURE__ */ jsx(RefreshCwIcon, { className: cn("size-3.5", props.loading && "animate-spin") })
2955
+ }
2956
+ ),
2957
+ /* @__PURE__ */ jsxs("div", { className: "flex h-7 min-w-0 flex-1 items-center gap-2 rounded-og-sm border border-og-border bg-og-surface-0 px-2 focus-within:border-og-accent/60", children: [
2958
+ /* @__PURE__ */ jsx(Globe2Icon, { className: "size-3 shrink-0 text-og-subtle" }),
2959
+ /* @__PURE__ */ jsx(
2960
+ "input",
2961
+ {
2962
+ value: draft,
2963
+ onChange: (event) => setDraft(event.target.value),
2964
+ onFocus: () => {
2965
+ focusedRef.current = true;
2966
+ },
2967
+ onBlur: () => {
2968
+ focusedRef.current = false;
2969
+ },
2970
+ onKeyDown: (event) => {
2971
+ if (event.key !== "Enter") return;
2972
+ event.preventDefault();
2973
+ const normalized = normalizeBrowserAddress(draft);
2974
+ if (normalized) props.onNavigate(normalized);
2975
+ },
2976
+ disabled: !props.target,
2977
+ className: "min-w-0 flex-1 bg-transparent text-og-control text-og-fg placeholder:text-og-subtle focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent disabled:opacity-50",
2978
+ "aria-label": "Address",
2979
+ spellCheck: false,
2980
+ placeholder: "Search or enter address"
2981
+ }
2982
+ )
2983
+ ] })
2984
+ ]
2985
+ }
2986
+ );
2987
+ }
2988
+ function BrowserViewport(props) {
2989
+ const canvasRef = useRef8(null);
2990
+ const inputRef = useRef8(null);
2991
+ const composingRef = useRef8(false);
2992
+ const pointerStartRef = useRef8(null);
2993
+ const lastClickRef = useRef8(null);
2994
+ const wheelRef = useRef8(null);
2995
+ const pendingTextRef = useRef8(null);
2996
+ const actionRef = useRef8(props.onAction);
2997
+ const readClipboardRef = useRef8(props.onReadClipboard);
2998
+ const errorRef = useRef8(props.onError);
2999
+ const actionTailRef = useRef8(Promise.resolve());
3000
+ const actionQueueEpochRef = useRef8(0);
3001
+ const queuedFrameRef = useRef8(null);
3002
+ const decodingFrameRef = useRef8(false);
3003
+ const mountedRef = useRef8(true);
3004
+ actionRef.current = props.onAction;
3005
+ readClipboardRef.current = props.onReadClipboard;
3006
+ errorRef.current = props.onError;
3007
+ const paintQueuedFrames = useCallback8(() => {
3008
+ if (decodingFrameRef.current) return;
3009
+ decodingFrameRef.current = true;
3010
+ void (async () => {
3011
+ try {
3012
+ while (mountedRef.current) {
3013
+ const frame = queuedFrameRef.current;
3014
+ queuedFrameRef.current = null;
3015
+ if (!frame) break;
3016
+ let objectUrl = null;
3017
+ try {
3018
+ const blob = new Blob([frame.data.slice().buffer], {
3019
+ type: frame.mediaType
3020
+ });
3021
+ if (typeof createImageBitmap === "function") {
3022
+ const bitmap = await createImageBitmap(blob);
3023
+ try {
3024
+ const canvas2 = canvasRef.current;
3025
+ if (mountedRef.current && canvas2) {
3026
+ paintCanvas(canvas2, bitmap, frame.width, frame.height);
3027
+ }
3028
+ } finally {
3029
+ bitmap.close();
3030
+ }
3031
+ continue;
3032
+ }
3033
+ objectUrl = URL.createObjectURL(blob);
3034
+ const image = await loadImage(objectUrl);
3035
+ const canvas = canvasRef.current;
3036
+ if (mountedRef.current && canvas) {
3037
+ paintCanvas(canvas, image, frame.width, frame.height);
3038
+ }
3039
+ } catch (cause) {
3040
+ if (mountedRef.current) errorRef.current(cause);
3041
+ } finally {
3042
+ if (objectUrl) URL.revokeObjectURL(objectUrl);
3043
+ }
3044
+ }
3045
+ } finally {
3046
+ decodingFrameRef.current = false;
3047
+ if (mountedRef.current && queuedFrameRef.current) paintQueuedFrames();
3048
+ }
3049
+ })();
3050
+ }, []);
3051
+ useEffect8(() => {
3052
+ mountedRef.current = true;
3053
+ return () => {
3054
+ mountedRef.current = false;
3055
+ queuedFrameRef.current = null;
3056
+ };
3057
+ }, []);
3058
+ useEffect8(() => {
3059
+ if (!props.frame) return;
3060
+ queuedFrameRef.current = props.frame;
3061
+ paintQueuedFrames();
3062
+ }, [paintQueuedFrames, props.frame]);
3063
+ useEffect8(
3064
+ () => () => {
3065
+ if (wheelRef.current?.timer) clearTimeout(wheelRef.current.timer);
3066
+ if (pendingTextRef.current?.timer) clearTimeout(pendingTextRef.current.timer);
3067
+ },
3068
+ []
3069
+ );
3070
+ const enqueue = useCallback8(
3071
+ (action, frame, after) => {
3072
+ const epoch = actionQueueEpochRef.current;
3073
+ actionTailRef.current = actionTailRef.current.catch(() => void 0).then(async () => {
3074
+ if (epoch !== actionQueueEpochRef.current) return;
3075
+ const receipt = await actionRef.current(action, frame);
3076
+ await after?.(receipt);
3077
+ }).catch((cause) => {
3078
+ actionQueueEpochRef.current += 1;
3079
+ errorRef.current(cause);
3080
+ });
3081
+ },
3082
+ []
3083
+ );
3084
+ const point = useCallback8(
3085
+ (frame, clientX, clientY) => browserPoint(canvasRef.current, frame, clientX, clientY),
3086
+ []
3087
+ );
3088
+ const flushPendingText = useCallback8(() => {
3089
+ const pending = pendingTextRef.current;
3090
+ if (!pending) return;
3091
+ clearTimeout(pending.timer);
3092
+ pendingTextRef.current = null;
3093
+ enqueue({ type: "type", text: pending.text }, pending.frame);
3094
+ }, [enqueue]);
3095
+ const flushPendingClick = useCallback8(() => {
3096
+ lastClickRef.current = null;
3097
+ }, []);
3098
+ const pointerDown = (event) => {
3099
+ if (!props.frame || event.button !== 0) return;
3100
+ event.preventDefault();
3101
+ flushPendingText();
3102
+ pointerStartRef.current = {
3103
+ x: event.clientX,
3104
+ y: event.clientY,
3105
+ pointerId: event.pointerId,
3106
+ frame: props.frame
3107
+ };
3108
+ event.currentTarget.setPointerCapture?.(event.pointerId);
3109
+ inputRef.current?.focus({ preventScroll: true });
3110
+ };
3111
+ const pointerUp = (event) => {
3112
+ const start = pointerStartRef.current;
3113
+ pointerStartRef.current = null;
3114
+ if (!start || start.pointerId !== event.pointerId) return;
3115
+ const from = point(start.frame, start.x, start.y);
3116
+ const to = point(start.frame, event.clientX, event.clientY);
3117
+ if (!from || !to) return;
3118
+ if (Math.hypot(event.clientX - start.x, event.clientY - start.y) > 5) {
3119
+ flushPendingClick();
3120
+ enqueue(
3121
+ {
3122
+ type: "pointer",
3123
+ action: "drag",
3124
+ x: from.x,
3125
+ y: from.y,
3126
+ endX: to.x,
3127
+ endY: to.y
3128
+ },
3129
+ start.frame
3130
+ );
3131
+ return;
3132
+ }
3133
+ const now = Date.now();
3134
+ const previous = lastClickRef.current;
3135
+ if (previous && now - previous.at < 280 && Math.hypot(previous.x - to.x, previous.y - to.y) < 6 && sameFrameFence(previous.frame, start.frame)) {
3136
+ lastClickRef.current = null;
3137
+ enqueue({ type: "pointer", action: "click", clickCount: 2, x: to.x, y: to.y }, start.frame);
3138
+ return;
3139
+ }
3140
+ lastClickRef.current = { at: now, x: to.x, y: to.y, frame: start.frame };
3141
+ enqueue({ type: "pointer", action: "click", x: to.x, y: to.y }, start.frame);
3142
+ };
3143
+ const contextMenu = (event) => {
3144
+ event.preventDefault();
3145
+ const frame = props.frame;
3146
+ if (!frame) return;
3147
+ flushPendingText();
3148
+ flushPendingClick();
3149
+ const at = point(frame, event.clientX, event.clientY);
3150
+ if (at) enqueue({ type: "pointer", action: "click", x: at.x, y: at.y, button: "right" }, frame);
3151
+ };
3152
+ const wheel = (event) => {
3153
+ const frame = props.frame;
3154
+ if (!frame) return;
3155
+ const at = point(frame, event.clientX, event.clientY);
3156
+ if (!at) return;
3157
+ flushPendingText();
3158
+ flushPendingClick();
3159
+ event.preventDefault();
3160
+ const pending = wheelRef.current;
3161
+ if (pending?.timer) clearTimeout(pending.timer);
3162
+ wheelRef.current = {
3163
+ x: at.x,
3164
+ y: at.y,
3165
+ deltaX: (pending && sameFrameFence(pending.frame, frame) ? pending.deltaX : 0) + event.deltaX,
3166
+ deltaY: (pending && sameFrameFence(pending.frame, frame) ? pending.deltaY : 0) + event.deltaY,
3167
+ frame,
3168
+ timer: setTimeout(() => {
3169
+ const batch = wheelRef.current;
3170
+ wheelRef.current = null;
3171
+ if (batch)
3172
+ enqueue(
3173
+ {
3174
+ type: "pointer",
3175
+ action: "scroll",
3176
+ x: batch.x,
3177
+ y: batch.y,
3178
+ deltaX: batch.deltaX,
3179
+ deltaY: batch.deltaY
3180
+ },
3181
+ batch.frame
3182
+ );
3183
+ }, 45)
3184
+ };
3185
+ };
3186
+ const keyDown = (event) => {
3187
+ const command = event.metaKey || event.ctrlKey;
3188
+ if (props.clipboardEnabled && command && !event.altKey && ["c", "v"].includes(event.key.toLowerCase())) {
3189
+ flushPendingText();
3190
+ return;
3191
+ }
3192
+ const key = browserKey(event);
3193
+ if (!key) return;
3194
+ event.preventDefault();
3195
+ flushPendingText();
3196
+ flushPendingClick();
3197
+ enqueue({ type: "press", key }, props.frame);
3198
+ };
3199
+ const copy = (event) => {
3200
+ if (!props.clipboardEnabled) return;
3201
+ event.preventDefault();
3202
+ flushPendingText();
3203
+ flushPendingClick();
3204
+ enqueue({ type: "clipboard", operation: "copy" }, props.frame, async () => {
3205
+ const clipboard = await readClipboardRef.current();
3206
+ if (clipboard.text.length === 0) return;
3207
+ if (!await copyTextToClipboard(clipboard.text)) {
3208
+ throw new Error("Browser text could not be copied to the local clipboard");
3209
+ }
3210
+ });
3211
+ };
3212
+ const paste = (event) => {
3213
+ if (!props.clipboardEnabled) return;
3214
+ event.preventDefault();
3215
+ flushPendingText();
3216
+ flushPendingClick();
3217
+ enqueue(
3218
+ {
3219
+ type: "clipboard",
3220
+ operation: "paste",
3221
+ text: event.clipboardData.getData("text/plain")
3222
+ },
3223
+ props.frame
3224
+ );
3225
+ };
3226
+ const input = (value, nativeComposing = false) => {
3227
+ if (composingRef.current || nativeComposing) return;
3228
+ if (!value) return;
3229
+ flushPendingClick();
3230
+ const pending = pendingTextRef.current;
3231
+ if (pending && sameOptionalBrowserFrame(pending.frame, props.frame)) {
3232
+ clearTimeout(pending.timer);
3233
+ pending.text += value;
3234
+ pending.timer = setTimeout(flushPendingText, 16);
3235
+ } else {
3236
+ flushPendingText();
3237
+ pendingTextRef.current = {
3238
+ text: value,
3239
+ frame: props.frame,
3240
+ timer: setTimeout(flushPendingText, 16)
3241
+ };
3242
+ }
3243
+ if (inputRef.current) inputRef.current.value = "";
3244
+ };
3245
+ const compositionStart = () => {
3246
+ composingRef.current = true;
3247
+ };
3248
+ const compositionEnd = (event) => {
3249
+ composingRef.current = false;
3250
+ input(event.currentTarget.value || event.data);
3251
+ };
3252
+ const showCanvas = props.frame !== null;
3253
+ return /* @__PURE__ */ jsxs("div", { className: "relative min-h-0 flex-1 overflow-hidden bg-black", children: [
3254
+ /* @__PURE__ */ jsx(
3255
+ "canvas",
3256
+ {
3257
+ ref: canvasRef,
3258
+ className: cn(
3259
+ "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",
3260
+ !showCanvas && "invisible"
3261
+ ),
3262
+ onPointerDown: pointerDown,
3263
+ onPointerUp: pointerUp,
3264
+ onPointerCancel: () => {
3265
+ pointerStartRef.current = null;
3266
+ },
3267
+ onContextMenu: contextMenu,
3268
+ onWheel: wheel,
3269
+ "aria-label": "Interactive browser page"
3270
+ }
3271
+ ),
3272
+ /* @__PURE__ */ jsx(
3273
+ "textarea",
3274
+ {
3275
+ ref: inputRef,
3276
+ defaultValue: "",
3277
+ onInput: (event) => input(event.currentTarget.value, event.nativeEvent.isComposing),
3278
+ onCompositionStart: compositionStart,
3279
+ onCompositionEnd: compositionEnd,
3280
+ onKeyDown: keyDown,
3281
+ onCopy: copy,
3282
+ onPaste: paste,
3283
+ className: "pointer-events-none absolute left-1/2 top-1/2 size-px resize-none overflow-hidden opacity-0",
3284
+ "aria-label": "Browser keyboard input",
3285
+ autoCapitalize: "off",
3286
+ autoCorrect: "off",
3287
+ spellCheck: false
3288
+ }
3289
+ ),
3290
+ !showCanvas ? /* @__PURE__ */ jsx(
3291
+ SemanticBrowserFallback,
3292
+ {
3293
+ observation: props.observation,
3294
+ connectionState: props.connectionState,
3295
+ supportsLiveFrames: props.supportsLiveFrames,
3296
+ error: props.connectionError,
3297
+ onAction: (action) => enqueue(action, null),
3298
+ onReconnect: props.onReconnect,
3299
+ reconnectLabel: props.reconnectLabel,
3300
+ reconnectMessage: props.reconnectMessage
3301
+ }
3302
+ ) : null,
3303
+ props.mutating ? /* @__PURE__ */ jsxs("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", children: [
3304
+ /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-3 animate-spin" }),
3305
+ " ",
3306
+ props.activityLabel ?? "Acting"
3307
+ ] }) : null
3308
+ ] });
3309
+ }
3310
+ function SemanticBrowserFallback(props) {
3311
+ const controlFailure = interactionControlFailureFromError(props.error);
3312
+ const generationLoss = Boolean(props.reconnectMessage) || isAttachedChromeGenerationLossError(props.error);
3313
+ const nodes = semanticNodes(
3314
+ props.observation?.semantic?.kind === "snapshot" ? props.observation.semantic.roots : []
3315
+ );
3316
+ const interactive = nodes.filter((node) => node.actions.includes("click")).slice(0, 8);
3317
+ return /* @__PURE__ */ jsx("div", { className: "absolute inset-0 grid place-items-center bg-og-bg p-6", children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-md rounded-og-lg border border-og-border bg-og-surface-0 p-4 shadow-lg", children: [
3318
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
3319
+ props.error ? /* @__PURE__ */ jsx(CircleAlertIcon, { className: "size-4 text-og-status-error" }) : !props.supportsLiveFrames ? /* @__PURE__ */ jsx(ZapIcon, { className: "size-4 text-og-muted" }) : /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-4 animate-spin text-og-muted" }),
3320
+ /* @__PURE__ */ jsx("p", { className: "text-og-menu font-medium text-og-fg", children: generationLoss ? "Chrome reconnected\u2014open a fresh browser/desktop." : props.error ? "Live view disconnected" : props.supportsLiveFrames ? browserConnectionLabel(props.connectionState) : "Semantic browser" })
3321
+ ] }),
3322
+ props.error || props.reconnectMessage ? /* @__PURE__ */ jsx("p", { className: "mt-2 text-og-control leading-5 text-og-muted", children: props.reconnectMessage ?? controlFailure?.message ?? props.error?.message }) : null,
3323
+ interactive.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "mt-3 border-t border-og-border pt-3", children: [
3324
+ /* @__PURE__ */ jsx("p", { className: "mb-2 text-og-xs text-og-subtle", children: props.supportsLiveFrames ? "Page controls remain available" : "Available page controls" }),
3325
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: interactive.map((node) => /* @__PURE__ */ jsx(
3326
+ "button",
3327
+ {
3328
+ type: "button",
3329
+ onClick: () => props.onAction({
3330
+ type: "click",
3331
+ locator: { kind: "ref", ref: node.ref }
3332
+ }),
3333
+ 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",
3334
+ children: node.name || node.role
3335
+ },
3336
+ node.ref
3337
+ )) })
3338
+ ] }) : null,
3339
+ props.error && props.supportsLiveFrames && (!generationLoss || props.reconnectLabel) ? /* @__PURE__ */ jsx(
3340
+ "button",
3341
+ {
3342
+ type: "button",
3343
+ onClick: props.onReconnect,
3344
+ className: "mt-3 text-og-control font-medium text-og-accent hover:underline",
3345
+ children: props.reconnectLabel ?? (controlFailure?.retryable === false ? "Try again" : "Reconnect")
3346
+ }
3347
+ ) : null
3348
+ ] }) });
3349
+ }
3350
+ function BrowserStatusBar(props) {
3351
+ const errors = (props.observation?.diagnostics.consoleErrorCount ?? 0) + (props.observation?.diagnostics.pageErrorCount ?? 0);
3352
+ const failed = props.observation?.diagnostics.failedRequestCount ?? 0;
3353
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-7 shrink-0 items-center gap-2 border-t border-og-border bg-og-surface-0 px-2 text-og-xs text-og-subtle", children: [
3354
+ /* @__PURE__ */ jsx(
3355
+ "span",
3356
+ {
3357
+ className: cn(
3358
+ "size-1.5 rounded-full",
3359
+ props.connectionState === "live" || props.connectionState === "semantic" ? "bg-og-status-running" : "bg-og-muted"
3360
+ )
3361
+ }
3362
+ ),
3363
+ /* @__PURE__ */ jsx("span", { children: props.connectionState === "live" ? "Live" : props.connectionState === "semantic" ? "Semantic" : browserConnectionLabel(props.connectionState) }),
3364
+ /* @__PURE__ */ jsx("span", { children: props.profile?.name ?? "Temporary browser" }),
3365
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: props.target?.title }),
3366
+ props.refreshing ? /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-3 animate-spin" }) : null,
3367
+ props.onOpenComputer ? /* @__PURE__ */ jsxs(
3368
+ "button",
3369
+ {
3370
+ type: "button",
3371
+ onClick: props.onOpenComputer,
3372
+ className: "flex items-center gap-1 rounded px-1.5 py-0.5 transition hover:bg-og-surface-2 hover:text-og-fg",
3373
+ "aria-label": "Open this browser window in Desktop",
3374
+ children: [
3375
+ /* @__PURE__ */ jsx(MonitorIcon, { className: "size-3" }),
3376
+ "Desktop"
3377
+ ]
3378
+ }
3379
+ ) : null,
3380
+ /* @__PURE__ */ jsxs(
3381
+ "button",
3382
+ {
3383
+ type: "button",
3384
+ onClick: props.onDiagnostics,
3385
+ "aria-expanded": props.diagnosticsOpen,
3386
+ "aria-controls": "browser-diagnostics-drawer",
3387
+ className: cn(
3388
+ "flex items-center gap-1 rounded px-1.5 py-0.5 transition hover:bg-og-surface-2 hover:text-og-fg",
3389
+ props.diagnosticsOpen && "bg-og-surface-2 text-og-fg"
3390
+ ),
3391
+ children: [
3392
+ /* @__PURE__ */ jsx(BugIcon, { className: "size-3" }),
3393
+ errors + failed > 0 ? errors + failed : "Debug"
3394
+ ]
3395
+ }
3396
+ )
3397
+ ] });
3398
+ }
3399
+ function BrowserDiagnosticsDrawer(props) {
3400
+ const session = props.session;
3401
+ const diagnostics = props.observation?.diagnostics;
3402
+ const semanticMode = props.observation?.semantic ? "Semantic page structure available" : props.observation?.screenshot ? "Visual fallback in use" : "Waiting for page observation";
3403
+ return /* @__PURE__ */ jsxs(
3404
+ "aside",
3405
+ {
3406
+ id: "browser-diagnostics-drawer",
3407
+ role: "region",
3408
+ "aria-label": "Browser diagnostics",
3409
+ className: "absolute bottom-7 right-0 top-10 z-40 flex w-full max-w-md flex-col border-l border-og-border bg-og-surface-0 shadow-2xl",
3410
+ children: [
3411
+ /* @__PURE__ */ jsxs("div", { className: "flex h-11 shrink-0 items-center gap-2 border-b border-og-border px-3", children: [
3412
+ /* @__PURE__ */ jsx(BugIcon, { className: "size-4 text-og-muted" }),
3413
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
3414
+ /* @__PURE__ */ jsx("p", { className: "text-og-control font-semibold text-og-fg", children: "Browser diagnostics" }),
3415
+ /* @__PURE__ */ jsx("p", { className: "truncate text-og-xs text-og-subtle", children: props.target?.title || shortUrl(props.target?.url ?? "") || "Current tab" })
3416
+ ] }),
3417
+ /* @__PURE__ */ jsx(
3418
+ "button",
3419
+ {
3420
+ type: "button",
3421
+ onClick: props.onRefresh,
3422
+ disabled: props.state.loading,
3423
+ 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",
3424
+ "aria-label": "Refresh browser diagnostics",
3425
+ children: /* @__PURE__ */ jsx(RefreshCwIcon, { className: cn("size-3.5", props.state.loading && "animate-spin") })
3426
+ }
3427
+ ),
3428
+ /* @__PURE__ */ jsx(
3429
+ "button",
3430
+ {
3431
+ type: "button",
3432
+ onClick: props.onClose,
3433
+ className: "grid size-7 place-items-center rounded-og-sm text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg",
3434
+ "aria-label": "Close browser diagnostics",
3435
+ children: /* @__PURE__ */ jsx(XIcon, { className: "size-3.5" })
3436
+ }
3437
+ )
3438
+ ] }),
3439
+ /* @__PURE__ */ jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto", children: [
3440
+ /* @__PURE__ */ jsxs("section", { className: "border-b border-og-border p-3", "aria-labelledby": "browser-runtime-title", children: [
3441
+ /* @__PURE__ */ jsx(
3442
+ "h3",
3443
+ {
3444
+ id: "browser-runtime-title",
3445
+ className: "text-[10px] font-semibold uppercase tracking-[0.14em] text-og-subtle",
3446
+ children: "Runtime"
3447
+ }
3448
+ ),
3449
+ /* @__PURE__ */ jsxs("dl", { className: "mt-2 grid grid-cols-[7rem_minmax(0,1fr)] gap-x-3 gap-y-1.5 text-og-xs", children: [
3450
+ /* @__PURE__ */ jsx(DiagnosticFact, { label: "Engine", value: browserEngineLabel(session) }),
3451
+ /* @__PURE__ */ jsx(DiagnosticFact, { label: "Driver", value: session?.driverId ?? "Unavailable" }),
3452
+ /* @__PURE__ */ jsx(DiagnosticFact, { label: "Placement", value: placementLabel(session ?? void 0) }),
3453
+ /* @__PURE__ */ jsx(
3454
+ DiagnosticFact,
3455
+ {
3456
+ label: "Profile",
3457
+ value: props.profile ? `${props.profile.name}${props.baseRevisionOrdinal ? ` \xB7 v${props.baseRevisionOrdinal}` : ""}` : "Temporary"
3458
+ }
3459
+ ),
3460
+ /* @__PURE__ */ jsx(
3461
+ DiagnosticFact,
3462
+ {
3463
+ label: "Controller",
3464
+ value: shortGeneration(session?.controller?.controllerGeneration)
3465
+ }
3466
+ ),
3467
+ /* @__PURE__ */ jsx(
3468
+ DiagnosticFact,
3469
+ {
3470
+ label: "Network",
3471
+ value: session?.networkRouteId ? "Custom route" : "Placement default"
3472
+ }
3473
+ ),
3474
+ /* @__PURE__ */ jsx(DiagnosticFact, { label: "Observation", value: semanticMode })
3475
+ ] })
3476
+ ] }),
3477
+ /* @__PURE__ */ jsxs("section", { className: "border-b border-og-border p-3", "aria-labelledby": "browser-page-title", children: [
3478
+ /* @__PURE__ */ jsx(
3479
+ "h3",
3480
+ {
3481
+ id: "browser-page-title",
3482
+ className: "text-[10px] font-semibold uppercase tracking-[0.14em] text-og-subtle",
3483
+ children: "Current page"
3484
+ }
3485
+ ),
3486
+ /* @__PURE__ */ jsxs("dl", { className: "mt-2 grid grid-cols-2 gap-2", children: [
3487
+ /* @__PURE__ */ jsx(DiagnosticCount, { label: "Console errors", value: diagnostics?.consoleErrorCount ?? 0 }),
3488
+ /* @__PURE__ */ jsx(DiagnosticCount, { label: "Page errors", value: diagnostics?.pageErrorCount ?? 0 }),
3489
+ /* @__PURE__ */ jsx(DiagnosticCount, { label: "Failed requests", value: diagnostics?.failedRequestCount ?? 0 }),
3490
+ /* @__PURE__ */ jsx(DiagnosticCount, { label: "Downloads", value: diagnostics?.downloadCount ?? 0 })
3491
+ ] }),
3492
+ session?.failureCode ? /* @__PURE__ */ jsxs("p", { className: "mt-2 rounded-og-sm border border-og-status-error/30 bg-og-status-error/5 px-2.5 py-2 text-og-xs text-og-status-error", children: [
3493
+ "Browser state: ",
3494
+ session.failureCode
3495
+ ] }) : null
3496
+ ] }),
3497
+ props.downloadsEnabled ? /* @__PURE__ */ jsx(
3498
+ BrowserDownloadsPanel,
3499
+ {
3500
+ downloads: props.downloads,
3501
+ loading: props.downloadsLoading,
3502
+ refreshing: props.downloadsRefreshing,
3503
+ error: props.downloadsError,
3504
+ savingDownloadIds: props.savingDownloadIds,
3505
+ onSave: props.onSaveDownload,
3506
+ onRefresh: props.onRefreshDownloads
3507
+ }
3508
+ ) : null,
3509
+ /* @__PURE__ */ jsxs("section", { className: "p-3", "aria-labelledby": "browser-events-title", children: [
3510
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
3511
+ /* @__PURE__ */ jsx(
3512
+ "h3",
3513
+ {
3514
+ id: "browser-events-title",
3515
+ className: "text-[10px] font-semibold uppercase tracking-[0.14em] text-og-subtle",
3516
+ children: "Recent events"
3517
+ }
3518
+ ),
3519
+ props.state.batch?.truncated ? /* @__PURE__ */ jsx("span", { className: "text-[10px] text-og-subtle", children: "Newest 100" }) : null
3520
+ ] }),
3521
+ props.state.loading && !props.state.batch ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 py-6 text-og-control text-og-muted", children: [
3522
+ /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-3.5 animate-spin" }),
3523
+ " Loading diagnostics\u2026"
3524
+ ] }) : props.state.error ? /* @__PURE__ */ jsxs("div", { className: "py-5", children: [
3525
+ /* @__PURE__ */ jsx("p", { className: "text-og-control text-og-status-error", children: props.state.error.message }),
3526
+ /* @__PURE__ */ jsx(
3527
+ "button",
3528
+ {
3529
+ type: "button",
3530
+ onClick: props.onRefresh,
3531
+ className: "mt-2 text-og-control font-medium text-og-accent hover:underline",
3532
+ children: "Try again"
3533
+ }
3534
+ )
3535
+ ] }) : props.state.batch?.entries.length ? /* @__PURE__ */ jsx("ol", { className: "mt-2 space-y-2", children: props.state.batch.entries.map((entry) => /* @__PURE__ */ jsxs(
3536
+ "li",
3537
+ {
3538
+ className: "rounded-og-sm border border-og-border bg-og-bg p-2.5",
3539
+ children: [
3540
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-[10px] text-og-subtle", children: [
3541
+ /* @__PURE__ */ jsx(
3542
+ "span",
3543
+ {
3544
+ className: cn(
3545
+ "font-semibold uppercase tracking-wide",
3546
+ diagnosticTone(entry.level)
3547
+ ),
3548
+ children: diagnosticKindLabel(entry.kind)
3549
+ }
3550
+ ),
3551
+ /* @__PURE__ */ jsx("span", { className: "ml-auto", children: formatDiagnosticTime(entry.occurredAt) })
3552
+ ] }),
3553
+ /* @__PURE__ */ jsx("p", { className: "mt-1 whitespace-pre-wrap break-words text-og-xs leading-5 text-og-fg", children: entry.message }),
3554
+ entry.url || entry.filename || entry.method || entry.status ? /* @__PURE__ */ jsx("p", { className: "mt-1 truncate font-og-mono text-[10px] text-og-subtle", children: [entry.method, entry.status, entry.filename, entry.url].filter((value) => value !== null).join(" \xB7 ") }) : null
3555
+ ]
3556
+ },
3557
+ entry.sequence
3558
+ )) }) : /* @__PURE__ */ jsx("p", { className: "py-6 text-og-control text-og-muted", children: "No diagnostics for this tab." })
3559
+ ] })
3560
+ ] })
3561
+ ]
3562
+ }
3563
+ );
3564
+ }
3565
+ function BrowserDownloadsPanel(props) {
3566
+ const [paths, setPaths] = useState8({});
3567
+ const [overwrite, setOverwrite] = useState8({});
3568
+ const [savedPaths, setSavedPaths] = useState8({});
3569
+ const [errors, setErrors] = useState8({});
3570
+ const saving = useMemo2(() => new Set(props.savingDownloadIds), [props.savingDownloadIds]);
3571
+ const save = (download) => {
3572
+ const destinationPath = (paths[download.id] ?? defaultDownloadDestination(download)).trim();
3573
+ if (!destinationPath || saving.has(download.id)) return;
3574
+ setErrors((current) => ({ ...current, [download.id]: "" }));
3575
+ void props.onSave(download.id, destinationPath, overwrite[download.id] ?? false).then((response) => {
3576
+ setSavedPaths((current) => ({ ...current, [download.id]: response.destinationPath }));
3577
+ }).catch((cause) => {
3578
+ setErrors((current) => ({
3579
+ ...current,
3580
+ [download.id]: cause instanceof Error ? cause.message : "Could not save this download."
3581
+ }));
3582
+ });
3583
+ };
3584
+ return /* @__PURE__ */ jsxs("section", { className: "border-b border-og-border p-3", "aria-labelledby": "browser-downloads-title", children: [
3585
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
3586
+ /* @__PURE__ */ jsx(DownloadIcon, { className: "size-3.5 text-og-muted" }),
3587
+ /* @__PURE__ */ jsx(
3588
+ "h3",
3589
+ {
3590
+ id: "browser-downloads-title",
3591
+ className: "text-[10px] font-semibold uppercase tracking-[0.14em] text-og-subtle",
3592
+ children: "Downloads"
3593
+ }
3594
+ ),
3595
+ props.refreshing ? /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "ml-auto size-3 animate-spin text-og-muted" }) : null
3596
+ ] }),
3597
+ props.loading ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 py-5 text-og-control text-og-muted", children: [
3598
+ /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-3.5 animate-spin" }),
3599
+ " Loading downloads\u2026"
3600
+ ] }) : props.error ? /* @__PURE__ */ jsxs("div", { className: "py-4", children: [
3601
+ /* @__PURE__ */ jsx("p", { className: "text-og-control text-og-status-error", children: props.error.message }),
3602
+ /* @__PURE__ */ jsx(
3603
+ "button",
3604
+ {
3605
+ type: "button",
3606
+ onClick: props.onRefresh,
3607
+ className: "mt-2 text-og-control font-medium text-og-accent hover:underline",
3608
+ children: "Try again"
3609
+ }
3610
+ )
3611
+ ] }) : props.downloads.length === 0 ? /* @__PURE__ */ jsx("p", { className: "py-4 text-og-control text-og-muted", children: "No files downloaded yet." }) : /* @__PURE__ */ jsx("ol", { className: "mt-2 space-y-2", children: props.downloads.map((download) => {
3612
+ const destinationPath = paths[download.id] ?? defaultDownloadDestination(download);
3613
+ const isSaving = saving.has(download.id);
3614
+ const isSaved = savedPaths[download.id] === destinationPath;
3615
+ return /* @__PURE__ */ jsxs(
3616
+ "li",
3617
+ {
3618
+ className: "rounded-og-sm border border-og-border bg-og-bg p-2.5",
3619
+ children: [
3620
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-start gap-2", children: [
3621
+ /* @__PURE__ */ jsx("span", { className: "mt-0.5 grid size-6 shrink-0 place-items-center rounded bg-og-surface-2 text-og-muted", children: isSaved ? /* @__PURE__ */ jsx(FileCheck2Icon, { className: "size-3.5 text-og-status-success" }) : /* @__PURE__ */ jsx(DownloadIcon, { className: "size-3.5" }) }),
3622
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
3623
+ /* @__PURE__ */ jsx(
3624
+ "p",
3625
+ {
3626
+ className: "truncate text-og-xs font-medium text-og-fg",
3627
+ title: download.filename,
3628
+ children: download.filename
3629
+ }
3630
+ ),
3631
+ /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-[10px] text-og-subtle", children: downloadStatusLabel(download) })
3632
+ ] })
3633
+ ] }),
3634
+ download.status === "completed" ? /* @__PURE__ */ jsxs("div", { className: "mt-2", children: [
3635
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-1.5", children: [
3636
+ /* @__PURE__ */ jsx(
3637
+ "input",
3638
+ {
3639
+ type: "text",
3640
+ value: destinationPath,
3641
+ onInput: (event) => {
3642
+ const value = event.currentTarget.value;
3643
+ setPaths((current) => ({ ...current, [download.id]: value }));
3644
+ setErrors((current) => ({ ...current, [download.id]: "" }));
3645
+ },
3646
+ onKeyDown: (event) => {
3647
+ if (event.key === "Enter") save(download);
3648
+ },
3649
+ disabled: isSaving,
3650
+ "aria-label": `Workspace path for ${download.filename}`,
3651
+ className: "h-7 min-w-0 flex-1 rounded-og-sm border border-og-border bg-og-surface-0 px-2 font-og-mono text-[10px] text-og-fg transition focus:border-og-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent disabled:opacity-50"
3652
+ }
3653
+ ),
3654
+ /* @__PURE__ */ jsxs(
3655
+ "button",
3656
+ {
3657
+ type: "button",
3658
+ onClick: () => save(download),
3659
+ disabled: isSaving || !destinationPath.trim() || isSaved && !overwrite[download.id],
3660
+ className: "inline-flex h-7 shrink-0 items-center gap-1 rounded-og-sm border border-og-border bg-og-surface-1 px-2 text-[10px] font-medium text-og-fg transition hover:bg-og-surface-2 disabled:opacity-40",
3661
+ children: [
3662
+ isSaving ? /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-3 animate-spin" }) : /* @__PURE__ */ jsx(SaveIcon, { className: "size-3" }),
3663
+ isSaving ? "Saving" : isSaved ? "Saved" : overwrite[download.id] ? "Replace" : "Save"
3664
+ ]
3665
+ }
3666
+ )
3667
+ ] }),
3668
+ /* @__PURE__ */ jsxs("label", { className: "mt-1.5 flex w-fit items-center gap-1.5 text-[10px] text-og-subtle", children: [
3669
+ /* @__PURE__ */ jsx(
3670
+ "input",
3671
+ {
3672
+ type: "checkbox",
3673
+ checked: overwrite[download.id] ?? false,
3674
+ onChange: (event) => {
3675
+ setOverwrite((current) => ({
3676
+ ...current,
3677
+ [download.id]: event.currentTarget.checked
3678
+ }));
3679
+ setErrors((current) => ({ ...current, [download.id]: "" }));
3680
+ },
3681
+ disabled: isSaving,
3682
+ className: "size-3 accent-og-accent"
3683
+ }
3684
+ ),
3685
+ "Replace an existing workspace file"
3686
+ ] }),
3687
+ errors[download.id] ? /* @__PURE__ */ jsx("p", { className: "mt-1.5 break-words text-[10px] text-og-status-error", children: errors[download.id] }) : null
3688
+ ] }) : null
3689
+ ]
3690
+ },
3691
+ download.id
3692
+ );
3693
+ }) })
3694
+ ] });
3695
+ }
3696
+ function downloadStatusLabel(download) {
3697
+ if (download.status === "completed") return formatBytes(download.receivedBytes);
3698
+ if (download.status === "in_progress") {
3699
+ const received = formatBytes(download.receivedBytes);
3700
+ return download.totalBytes === null ? `Downloading \xB7 ${received}` : `Downloading \xB7 ${received} of ${formatBytes(download.totalBytes)}`;
3701
+ }
3702
+ if (download.status === "cancelled") return "Cancelled";
3703
+ if (download.status === "unavailable") return "No longer available";
3704
+ return "Download failed";
3705
+ }
3706
+ function defaultDownloadDestination(download) {
3707
+ const leaf = download.filename.split(/[\\/]/u).at(-1)?.trim() ?? "";
3708
+ const portable = leaf.normalize("NFC").replace(/[<>:"|?*\u0000-\u001f\u007f]/gu, "-").replace(/[ .]+$/u, "");
3709
+ if (portable.length === 0 || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu.test(portable)) {
3710
+ return `download-${download.id.slice(0, 8)}`;
3711
+ }
3712
+ return portable;
3713
+ }
3714
+ function DiagnosticFact(props) {
3715
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
3716
+ /* @__PURE__ */ jsx("dt", { className: "text-og-subtle", children: props.label }),
3717
+ /* @__PURE__ */ jsx("dd", { className: "min-w-0 truncate text-og-fg", title: props.value, children: props.value })
3718
+ ] });
3719
+ }
3720
+ function DiagnosticCount(props) {
3721
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-og-sm border border-og-border bg-og-bg px-2.5 py-2", children: [
3722
+ /* @__PURE__ */ jsx("dt", { className: "text-[10px] text-og-subtle", children: props.label }),
3723
+ /* @__PURE__ */ jsx(
3724
+ "dd",
3725
+ {
3726
+ className: cn(
3727
+ "mt-0.5 text-og-sm font-semibold",
3728
+ props.value ? "text-og-fg" : "text-og-muted"
3729
+ ),
3730
+ children: props.value
3731
+ }
3732
+ )
3733
+ ] });
3734
+ }
3735
+ function browserEngineLabel(session) {
3736
+ if (!session) return "Unavailable";
3737
+ const version = session.engineVersion ? ` ${session.engineVersion}` : "";
3738
+ return `${session.engine}${version} \xB7 ${session.headless ? "headless" : "headed"}`;
3739
+ }
3740
+ function shortGeneration(value) {
3741
+ if (!value) return "Unavailable";
3742
+ return value.length > 16 ? `${value.slice(0, 8)}\u2026${value.slice(-6)}` : value;
3743
+ }
3744
+ function diagnosticKindLabel(kind) {
3745
+ switch (kind) {
3746
+ case "console":
3747
+ return "Console";
3748
+ case "page_error":
3749
+ return "Page error";
3750
+ case "failed_request":
3751
+ return "Request";
3752
+ case "download":
3753
+ return "Download";
3754
+ }
3755
+ }
3756
+ function diagnosticTone(level) {
3757
+ if (level === "error") return "text-og-status-error";
3758
+ if (level === "warning") return "text-og-status-waiting";
3759
+ return "text-og-muted";
3760
+ }
3761
+ function formatDiagnosticTime(value) {
3762
+ const date = new Date(value);
3763
+ return Number.isNaN(date.getTime()) ? "" : date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
3764
+ }
3765
+ function BrowserNotice(props) {
3766
+ return /* @__PURE__ */ jsx(
3767
+ "div",
3768
+ {
3769
+ className: cn(
3770
+ "grid h-full place-items-center bg-og-bg text-og-control text-og-muted",
3771
+ props.className
3772
+ ),
3773
+ children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
3774
+ props.icon,
3775
+ props.text
3776
+ ] })
3777
+ }
3778
+ );
3779
+ }
3780
+ function paintCanvas(canvas, image, width, height) {
3781
+ canvas.width = width;
3782
+ canvas.height = height;
3783
+ const context = canvas.getContext("2d", { alpha: false });
3784
+ if (!context) throw new Error("Browser canvas is unavailable.");
3785
+ context.drawImage(image, 0, 0, width, height);
3786
+ }
3787
+ function loadImage(url) {
3788
+ return new Promise((resolve, reject) => {
3789
+ const image = new Image();
3790
+ image.onload = () => resolve(image);
3791
+ image.onerror = () => reject(new Error("Browser frame image could not be decoded."));
3792
+ image.src = url;
3793
+ });
3794
+ }
3795
+ function browserPoint(canvas, frame, clientX, clientY) {
3796
+ if (!canvas || !frame) return null;
3797
+ const rect = canvas.getBoundingClientRect();
3798
+ if (rect.width <= 0 || rect.height <= 0) return null;
3799
+ const pixelX = (clientX - rect.left) / rect.width * frame.width;
3800
+ const pixelY = (clientY - rect.top) / rect.height * frame.height;
3801
+ return {
3802
+ x: Math.max(0, pixelX / frame.deviceScaleFactor),
3803
+ y: Math.max(0, pixelY / frame.deviceScaleFactor)
3804
+ };
3805
+ }
3806
+ function semanticNodes(roots) {
3807
+ const result = [];
3808
+ const stack = [...roots].reverse();
3809
+ while (stack.length > 0) {
3810
+ const node = stack.pop();
3811
+ result.push(node);
3812
+ if (node.children) stack.push(...[...node.children].reverse());
3813
+ }
3814
+ return result;
3815
+ }
3816
+ function frameMatchesSelectedTarget(frame, session, target) {
3817
+ return Boolean(
3818
+ frame && session && target && frame.browserSessionId === session.id && frame.targetId === target.id && frame.targetGeneration === target.targetGeneration && frame.documentGeneration === target.documentGeneration
3819
+ );
3820
+ }
3821
+ function sameFrameFence(left, right) {
3822
+ return left.browserSessionId === right.browserSessionId && left.controllerGeneration === right.controllerGeneration && left.targetId === right.targetId && left.targetGeneration === right.targetGeneration && left.documentGeneration === right.documentGeneration && left.frameId === right.frameId;
3823
+ }
3824
+ function sameOptionalBrowserFrame(left, right) {
3825
+ return left === null || right === null ? left === right : sameFrameFence(left, right);
3826
+ }
3827
+ function isLiveBrowser2(session) {
3828
+ return !["ending", "ended", "failed", "lost"].includes(session.lifecycle);
3829
+ }
3830
+ function attachedChromeGenerationLoss(sessions) {
3831
+ const lost = sessions.find(
3832
+ (session) => session.lifecycle === "lost" && session.failureCode === "controller_transition_expired" && session.placement.kind === "attached_device"
3833
+ );
3834
+ return lost?.placement.kind === "attached_device" ? { deviceId: lost.placement.deviceId } : null;
3835
+ }
3836
+ function isAttachedChromeGenerationLossError(error) {
3837
+ if (!error) return false;
3838
+ return /placement instance changed|controller authority changed/i.test(error.message);
3839
+ }
3840
+ function countInterventions(interventions) {
3841
+ const counts = /* @__PURE__ */ new Map();
3842
+ for (const intervention of interventions) {
3843
+ counts.set(intervention.resourceId, (counts.get(intervention.resourceId) ?? 0) + 1);
3844
+ }
3845
+ return counts;
3846
+ }
3847
+ function interventionTitle(intervention) {
3848
+ switch (intervention.kind) {
3849
+ case "manual_login":
3850
+ return "Sign in needed";
3851
+ case "mfa":
3852
+ return "Verification needed";
3853
+ case "external_action":
3854
+ case "confirmation":
3855
+ return "Action needed";
3856
+ case "other":
3857
+ return "Browser needs your help";
3858
+ }
3859
+ }
3860
+ function placementLabel(session) {
3861
+ if (!session) return "";
3862
+ switch (session.placement.kind) {
3863
+ case "sandbox_group":
3864
+ return "Agent sandbox";
3865
+ case "connected_machine":
3866
+ return "Connected machine";
3867
+ case "attached_device":
3868
+ return "Your browser";
3869
+ case "external_provider":
3870
+ return "Cloud browser";
3871
+ }
3872
+ }
3873
+ function browserConnectionLabel(state) {
3874
+ switch (state) {
3875
+ case "attaching":
3876
+ return "Opening browser\u2026";
3877
+ case "connecting":
3878
+ return "Connecting\u2026";
3879
+ case "reconnecting":
3880
+ return "Reconnecting\u2026";
3881
+ case "error":
3882
+ return "Disconnected";
3883
+ case "semantic":
3884
+ return "Semantic";
3885
+ default:
3886
+ return "Waiting for browser\u2026";
3887
+ }
3888
+ }
3889
+ function shortUrl(value) {
3890
+ try {
3891
+ return new URL(value).hostname || value;
3892
+ } catch {
3893
+ return value;
3894
+ }
3895
+ }
3896
+
3897
+ export {
3898
+ useBrowserFrameStream,
3899
+ useAttachedBrowsers,
3900
+ useBrowserIdentities,
3901
+ useBrowserDownloads,
3902
+ useBrowserSession,
3903
+ useBrowserSessions,
3904
+ useSiteAuthConnections,
3905
+ BrowserViewer
3906
+ };
3907
+ //# sourceMappingURL=chunk-X4JAFD5R.js.map