@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,2300 @@
1
+ import {
2
+ InteractionInterventionBanner,
3
+ isNonRetryableInteractionError,
4
+ isSourcePlacementChangedError,
5
+ useInteractionInterventions,
6
+ useInteractionInvalidation
7
+ } from "./chunk-H5YGYNXS.js";
8
+ import {
9
+ DesktopViewer
10
+ } from "./chunk-4MDLEHXL.js";
11
+ import {
12
+ STREAM_CLOSE_REASON_NORMAL,
13
+ STREAM_KIND_COMPUTER,
14
+ STREAM_ROLE_CLIENT,
15
+ decodeStreamFrame,
16
+ decodeStreamOpenAck,
17
+ encodeStreamClose,
18
+ encodeStreamOpen
19
+ } from "./chunk-5YK2ZDIG.js";
20
+ import {
21
+ usePageLiveActivity
22
+ } from "./chunk-KZ73RL76.js";
23
+ import {
24
+ useEmbeddedComputerInteraction
25
+ } from "./chunk-NEXLLTP4.js";
26
+ import {
27
+ cn
28
+ } from "./chunk-22KP6LR5.js";
29
+ import {
30
+ copyTextToClipboard
31
+ } from "./chunk-JVYQRRC4.js";
32
+
33
+ // src/components/computer-viewer.tsx
34
+ import { interactionControlFailureFromError } from "@opengeni/sdk/interaction";
35
+ import {
36
+ ChevronDownIcon,
37
+ CircleAlertIcon,
38
+ KeyboardIcon,
39
+ LoaderCircleIcon,
40
+ LockKeyholeIcon,
41
+ MonitorIcon,
42
+ MousePointer2Icon,
43
+ PanelsTopLeftIcon,
44
+ PlusIcon,
45
+ RefreshCwIcon,
46
+ RotateCcwIcon
47
+ } from "lucide-react";
48
+ import {
49
+ useCallback as useCallback4,
50
+ useEffect as useEffect4,
51
+ useMemo as useMemo2,
52
+ useRef as useRef4,
53
+ useState as useState4
54
+ } from "react";
55
+
56
+ // src/hooks/use-computer-frame-stream.ts
57
+ import {
58
+ computerFrameSocketUrl,
59
+ decodeComputerFrameMessage
60
+ } from "@opengeni/sdk/interaction";
61
+ import { useCallback, useEffect, useRef, useState } from "react";
62
+ var RELAY_TAG_OPEN = 1;
63
+ var RELAY_TAG_OPEN_ACK = 2;
64
+ var RELAY_TAG_FRAME = 3;
65
+ var RELAY_TAG_CLOSE = 4;
66
+ var MAX_AUTOMATIC_RECONNECTS_WITHOUT_A_FRAME = 2;
67
+ function useComputerFrameStream(options) {
68
+ const { client, workspaceId } = useEmbeddedComputerInteraction(options);
69
+ const enabled = options.enabled ?? true;
70
+ const [nonce, setNonce] = useState(0);
71
+ const [result, setResult] = useState({
72
+ state: "idle",
73
+ frame: null,
74
+ attachment: null,
75
+ error: null
76
+ });
77
+ const latestRef = useRef({
78
+ key: "",
79
+ sequence: -1
80
+ });
81
+ const factoryRef = useRef(options.webSocketFactory);
82
+ const streamRef = useRef(options.stream);
83
+ factoryRef.current = options.webSocketFactory;
84
+ streamRef.current = options.stream;
85
+ const reconnect = useCallback(() => setNonce((value) => value + 1), []);
86
+ useEffect(() => {
87
+ const computerSessionId = options.computerSessionId;
88
+ const targetId = options.targetId;
89
+ if (!enabled || !computerSessionId || !targetId || typeof window === "undefined") {
90
+ setResult({ state: "idle", frame: null, attachment: null, error: null });
91
+ return;
92
+ }
93
+ latestRef.current = { key: "", sequence: -1 };
94
+ setResult({
95
+ state: "attaching",
96
+ frame: null,
97
+ attachment: null,
98
+ error: null
99
+ });
100
+ let disposed = false;
101
+ let socket = null;
102
+ let socketHandlers = null;
103
+ let reconnectTimer = null;
104
+ let expiryTimer = null;
105
+ let failures = 0;
106
+ let controllerGeneration = null;
107
+ let activeAttachment = null;
108
+ let activeStream = null;
109
+ let attachmentExpiresAt = 0;
110
+ let lastRelaySequence = null;
111
+ let relayAccepted = false;
112
+ let pendingFrame = null;
113
+ let decodingFrame = false;
114
+ const attachmentAbort = new AbortController();
115
+ const clearSocket = (terminateProducer = false) => {
116
+ if (!socket) return;
117
+ const current = socket;
118
+ const handlers = socketHandlers;
119
+ if (terminateProducer && activeStream?.kind === "relay" && socket.readyState === 1) {
120
+ try {
121
+ socket.send(
122
+ relayDatagram(
123
+ RELAY_TAG_CLOSE,
124
+ encodeStreamClose({
125
+ channelId: activeStream.channel.channelId,
126
+ reason: STREAM_CLOSE_REASON_NORMAL,
127
+ message: "viewer detached"
128
+ })
129
+ )
130
+ );
131
+ } catch {
132
+ }
133
+ }
134
+ if (handlers) {
135
+ current.removeEventListener("open", handlers.open);
136
+ current.removeEventListener("message", handlers.message);
137
+ current.removeEventListener("error", handlers.error);
138
+ current.removeEventListener("close", handlers.close);
139
+ }
140
+ if (current.readyState === 0 || current.readyState === 1) {
141
+ current.close(1e3, "viewer detached");
142
+ }
143
+ socket = null;
144
+ socketHandlers = null;
145
+ pendingFrame = null;
146
+ };
147
+ const scheduleReconnect = () => {
148
+ if (disposed || reconnectTimer) return;
149
+ if (failures >= MAX_AUTOMATIC_RECONNECTS_WITHOUT_A_FRAME) return;
150
+ const delay = Math.min(5e3, 250 * 2 ** Math.min(failures, 5));
151
+ failures += 1;
152
+ setResult((current) => ({ ...current, state: "reconnecting" }));
153
+ reconnectTimer = setTimeout(() => {
154
+ reconnectTimer = null;
155
+ connectSocket();
156
+ }, delay);
157
+ };
158
+ const fail = (cause, reconnectAutomatically = true) => {
159
+ if (disposed) return;
160
+ const error = cause instanceof Error ? cause : new Error(String(cause));
161
+ setResult((current) => ({ ...current, state: "error", error }));
162
+ clearSocket();
163
+ if (reconnectAutomatically) scheduleReconnect();
164
+ };
165
+ const onOpen = (source) => {
166
+ if (disposed || source !== socket) return;
167
+ if (activeStream?.kind === "relay") {
168
+ const body = encodeStreamOpen({
169
+ channel: { ...activeStream.channel, kind: STREAM_KIND_COMPUTER },
170
+ token: activeStream.token,
171
+ role: STREAM_ROLE_CLIENT,
172
+ resumeFromSeq: lastRelaySequence === null ? "0" : (BigInt(lastRelaySequence) + 1n).toString()
173
+ });
174
+ try {
175
+ source.send(relayDatagram(RELAY_TAG_OPEN, body));
176
+ } catch (error) {
177
+ fail(error);
178
+ }
179
+ return;
180
+ }
181
+ setResult((current) => ({ ...current, state: "live", error: null }));
182
+ };
183
+ const drainLatestFrame = async () => {
184
+ if (decodingFrame) return;
185
+ decodingFrame = true;
186
+ try {
187
+ while (pendingFrame) {
188
+ if (disposed) break;
189
+ const pending = pendingFrame;
190
+ pendingFrame = null;
191
+ const { bytes, source } = pending;
192
+ const frame = await decodeComputerFrameMessage(bytes);
193
+ if (disposed || source !== socket) continue;
194
+ if (frame.computerSessionId !== computerSessionId || frame.targetId !== targetId) {
195
+ throw new Error("desktop frame belongs to another resource");
196
+ }
197
+ if (!controllerGeneration || frame.controllerGeneration !== controllerGeneration) {
198
+ throw new Error("desktop frame belongs to a stale controller");
199
+ }
200
+ const key = `${computerSessionId}:${targetId}:${frame.controllerGeneration}:${frame.targetGeneration}`;
201
+ if (latestRef.current.key === key && frame.sequence <= latestRef.current.sequence)
202
+ continue;
203
+ latestRef.current = { key, sequence: frame.sequence };
204
+ failures = 0;
205
+ setResult((current) => ({
206
+ ...current,
207
+ state: "live",
208
+ frame,
209
+ error: null
210
+ }));
211
+ }
212
+ } catch (cause) {
213
+ fail(cause);
214
+ } finally {
215
+ decodingFrame = false;
216
+ if (!disposed && pendingFrame) void drainLatestFrame();
217
+ }
218
+ };
219
+ const onMessage = (source, event) => {
220
+ void (async () => {
221
+ try {
222
+ const bytes = await messageBytes(event.data);
223
+ if (disposed || source !== socket) return;
224
+ let frameBytes = bytes;
225
+ if (activeStream?.kind === "relay") {
226
+ if (bytes.length < 1) throw new Error("desktop relay returned an empty message");
227
+ const tag = bytes[0];
228
+ const body = bytes.subarray(1);
229
+ if (tag === RELAY_TAG_OPEN_ACK) {
230
+ const ack = decodeStreamOpenAck(body);
231
+ if (!ack.accepted) {
232
+ activeAttachment = null;
233
+ activeStream = null;
234
+ attachmentExpiresAt = 0;
235
+ throw new Error(ack.error?.message ?? "desktop stream was rejected by the relay");
236
+ }
237
+ relayAccepted = true;
238
+ return;
239
+ }
240
+ if (tag === RELAY_TAG_CLOSE) {
241
+ activeAttachment = null;
242
+ activeStream = null;
243
+ attachmentExpiresAt = 0;
244
+ lastRelaySequence = null;
245
+ relayAccepted = false;
246
+ fail(new Error("Desktop frame source ended."), false);
247
+ return;
248
+ }
249
+ if (tag !== RELAY_TAG_FRAME || !relayAccepted) return;
250
+ const relayFrame = decodeStreamFrame(body);
251
+ lastRelaySequence = relayFrame.seq;
252
+ frameBytes = relayFrame.data;
253
+ }
254
+ pendingFrame = { bytes: frameBytes, source };
255
+ void drainLatestFrame();
256
+ } catch (cause) {
257
+ fail(cause);
258
+ }
259
+ })();
260
+ };
261
+ const onError = (source) => {
262
+ if (source !== socket) return;
263
+ fail(new Error("Desktop view lost connection."));
264
+ };
265
+ const onClose = (source) => {
266
+ if (disposed || source !== socket) return;
267
+ clearSocket();
268
+ scheduleReconnect();
269
+ };
270
+ const connectSocket = () => {
271
+ if (disposed) return;
272
+ clearSocket();
273
+ if (!activeAttachment || !activeStream || attachmentExpiresAt <= Date.now() + 1e3) {
274
+ void attach();
275
+ return;
276
+ }
277
+ if (activeStream.kind === "direct_rfb") {
278
+ setResult((current) => ({ ...current, state: "live", error: null }));
279
+ return;
280
+ }
281
+ relayAccepted = false;
282
+ setResult((current) => ({
283
+ ...current,
284
+ state: "connecting",
285
+ error: null
286
+ }));
287
+ const createSocket = factoryRef.current ?? ((url, protocols) => new WebSocket(url, protocols));
288
+ const openedSocket = createSocket(
289
+ activeStream.kind === "relay" ? activeStream.url : computerFrameSocketUrl(activeAttachment, streamRef.current),
290
+ activeStream.kind === "direct_websocket" ? [...activeStream.protocols] : []
291
+ );
292
+ socket = openedSocket;
293
+ socketHandlers = {
294
+ open: () => onOpen(openedSocket),
295
+ message: (event) => onMessage(openedSocket, event),
296
+ error: () => onError(openedSocket),
297
+ close: () => onClose(openedSocket)
298
+ };
299
+ openedSocket.binaryType = "arraybuffer";
300
+ openedSocket.addEventListener("open", socketHandlers.open);
301
+ openedSocket.addEventListener("message", socketHandlers.message);
302
+ openedSocket.addEventListener("error", socketHandlers.error);
303
+ openedSocket.addEventListener("close", socketHandlers.close);
304
+ };
305
+ const attach = async () => {
306
+ if (disposed) return;
307
+ clearSocket();
308
+ if (expiryTimer) clearTimeout(expiryTimer);
309
+ setResult((current) => ({
310
+ ...current,
311
+ state: current.attachment ? "reconnecting" : "attaching",
312
+ error: null
313
+ }));
314
+ try {
315
+ const attachment = await client.attachComputerSession(
316
+ workspaceId,
317
+ computerSessionId,
318
+ {
319
+ targetId,
320
+ expiresInSeconds: 120,
321
+ ...streamRef.current ? { stream: streamRef.current } : {}
322
+ },
323
+ { signal: attachmentAbort.signal }
324
+ );
325
+ if (disposed) return;
326
+ if (attachment.computerSessionId !== computerSessionId || attachment.targetId !== targetId) {
327
+ throw new Error("desktop attachment does not match the requested resource");
328
+ }
329
+ controllerGeneration = attachment.controllerGeneration;
330
+ activeAttachment = attachment;
331
+ activeStream = attachment.stream;
332
+ attachmentExpiresAt = Date.parse(attachment.expiresAt);
333
+ lastRelaySequence = null;
334
+ relayAccepted = false;
335
+ setResult((current) => ({
336
+ ...current,
337
+ state: "connecting",
338
+ attachment: {
339
+ computerSessionId: attachment.computerSessionId,
340
+ controllerGeneration: attachment.controllerGeneration,
341
+ targetId: attachment.targetId,
342
+ stream: attachment.stream,
343
+ expiresAt: attachment.expiresAt
344
+ },
345
+ error: null
346
+ }));
347
+ const refreshIn = Number.isFinite(attachmentExpiresAt) ? Math.max(1e3, attachmentExpiresAt - Date.now() - 5e3) : 6e4;
348
+ expiryTimer = setTimeout(() => {
349
+ if (disposed) return;
350
+ clearSocket(true);
351
+ activeAttachment = null;
352
+ activeStream = null;
353
+ attachmentExpiresAt = 0;
354
+ void attach();
355
+ }, refreshIn);
356
+ connectSocket();
357
+ } catch (cause) {
358
+ if (attachmentAbort.signal.aborted || disposed) return;
359
+ fail(cause, !isPlacementGenerationLossError(cause));
360
+ }
361
+ };
362
+ void attach();
363
+ return () => {
364
+ disposed = true;
365
+ pendingFrame = null;
366
+ attachmentAbort.abort();
367
+ if (reconnectTimer) clearTimeout(reconnectTimer);
368
+ if (expiryTimer) clearTimeout(expiryTimer);
369
+ clearSocket(true);
370
+ };
371
+ }, [client, enabled, nonce, options.computerSessionId, options.targetId, workspaceId]);
372
+ return { ...result, reconnect };
373
+ }
374
+ function relayDatagram(tag, body) {
375
+ const message = new Uint8Array(body.length + 1);
376
+ message[0] = tag;
377
+ message.set(body, 1);
378
+ return message.buffer;
379
+ }
380
+ async function messageBytes(value) {
381
+ if (value instanceof ArrayBuffer) return new Uint8Array(value);
382
+ if (ArrayBuffer.isView(value)) {
383
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength).slice();
384
+ }
385
+ if (typeof Blob !== "undefined" && value instanceof Blob) {
386
+ return new Uint8Array(await value.arrayBuffer());
387
+ }
388
+ throw new Error("desktop frame stream returned a non-binary message");
389
+ }
390
+ function isPlacementGenerationLossError(cause) {
391
+ const message = cause instanceof Error ? cause.message : String(cause);
392
+ return /placement instance changed|controller authority changed/i.test(message);
393
+ }
394
+
395
+ // src/hooks/use-computer-session.ts
396
+ import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
397
+ function useComputerSession(options) {
398
+ const { client, workspaceId } = useEmbeddedComputerInteraction(options);
399
+ const computerSessionId = options.computerSessionId;
400
+ const enabled = (options.enabled ?? true) && computerSessionId !== null;
401
+ const pageLive = usePageLiveActivity();
402
+ const pollIntervalMs = Math.max(750, options.pollIntervalMs ?? 2e3);
403
+ const [state, setState] = useState2(
404
+ () => emptyState(computerSessionId, enabled)
405
+ );
406
+ const visible = state.computerSessionId === computerSessionId ? state : emptyState(computerSessionId, enabled);
407
+ const refreshBlocked = isNonRetryableInteractionError(visible.error);
408
+ const selectedTargetIdRef = useRef2(visible.selectedTargetId);
409
+ selectedTargetIdRef.current = visible.selectedTargetId;
410
+ const targetsRef = useRef2({
411
+ computerSessionId,
412
+ targets: visible.targets
413
+ });
414
+ const observationRef = useRef2({ computerSessionId, observation: visible.observation });
415
+ if (targetsRef.current.computerSessionId !== computerSessionId) {
416
+ targetsRef.current = { computerSessionId, targets: visible.targets };
417
+ observationRef.current = {
418
+ computerSessionId,
419
+ observation: visible.observation
420
+ };
421
+ } else {
422
+ targetsRef.current.targets = visible.targets;
423
+ observationRef.current.observation = visible.observation;
424
+ }
425
+ const requestRef = useRef2({
426
+ id: 0,
427
+ controller: null
428
+ });
429
+ const mutationRef = useRef2({
430
+ computerSessionId,
431
+ count: 0
432
+ });
433
+ const mountedRef = useRef2(true);
434
+ const invalidateRefresh = useCallback2(() => {
435
+ const id = requestRef.current.id + 1;
436
+ requestRef.current.controller?.abort();
437
+ requestRef.current = { id, controller: null };
438
+ }, []);
439
+ const refresh = useCallback2(async () => {
440
+ if (!enabled || !computerSessionId) return;
441
+ const id = requestRef.current.id + 1;
442
+ requestRef.current.controller?.abort();
443
+ const controller = new AbortController();
444
+ requestRef.current = { id, controller };
445
+ try {
446
+ const [session, targetResponse] = await Promise.all([
447
+ client.getComputerSession(workspaceId, computerSessionId, {
448
+ signal: controller.signal
449
+ }),
450
+ client.listComputerTargets(workspaceId, computerSessionId, {
451
+ signal: controller.signal
452
+ })
453
+ ]);
454
+ if (!mountedRef.current || requestRef.current.id !== id) return;
455
+ const targets = sortComputerTargets(targetResponse.targets);
456
+ const selected = chooseTarget(targets, selectedTargetIdRef.current, session.platform);
457
+ const observation = selected ? await client.observeComputerTarget(workspaceId, computerSessionId, selected.id, {
458
+ signal: controller.signal
459
+ }) : null;
460
+ if (!mountedRef.current || requestRef.current.id !== id) return;
461
+ selectedTargetIdRef.current = selected?.id ?? null;
462
+ targetsRef.current = { computerSessionId, targets };
463
+ observationRef.current = { computerSessionId, observation };
464
+ setState(
465
+ (current) => current.computerSessionId === computerSessionId ? {
466
+ ...current,
467
+ session,
468
+ targets,
469
+ selectedTargetId: selected?.id ?? null,
470
+ observation,
471
+ loading: false,
472
+ error: null
473
+ } : current
474
+ );
475
+ } catch (cause) {
476
+ if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id) return;
477
+ setState(
478
+ (current) => current.computerSessionId === computerSessionId ? {
479
+ ...current,
480
+ loading: false,
481
+ error: cause instanceof Error ? cause : new Error(String(cause))
482
+ } : current
483
+ );
484
+ } finally {
485
+ if (requestRef.current.id === id) requestRef.current = { id, controller: null };
486
+ }
487
+ }, [client, computerSessionId, enabled, workspaceId]);
488
+ useEffect2(() => {
489
+ mountedRef.current = true;
490
+ if (!enabled) {
491
+ invalidateRefresh();
492
+ setState(emptyState(computerSessionId, false));
493
+ return;
494
+ }
495
+ setState(
496
+ (current) => current.computerSessionId === computerSessionId ? { ...current, loading: true } : emptyState(computerSessionId, true)
497
+ );
498
+ void refresh();
499
+ return () => {
500
+ mountedRef.current = false;
501
+ invalidateRefresh();
502
+ };
503
+ }, [computerSessionId, enabled, invalidateRefresh, refresh]);
504
+ useEffect2(() => {
505
+ if (!enabled || !pageLive || visible.mutating || refreshBlocked) return;
506
+ const timer = setInterval(() => {
507
+ if (!requestRef.current.controller) void refresh();
508
+ }, pollIntervalMs);
509
+ return () => clearInterval(timer);
510
+ }, [enabled, pageLive, pollIntervalMs, refresh, refreshBlocked, visible.mutating]);
511
+ useEffect2(() => {
512
+ if (!enabled || !computerSessionId || !pageLive || refreshBlocked) return;
513
+ let disposed = false;
514
+ const timer = setInterval(() => {
515
+ void client.heartbeatComputerSession(workspaceId, computerSessionId).catch(() => {
516
+ if (!disposed) void refresh();
517
+ });
518
+ }, 3e4);
519
+ return () => {
520
+ disposed = true;
521
+ clearInterval(timer);
522
+ };
523
+ }, [client, computerSessionId, enabled, pageLive, refresh, refreshBlocked, workspaceId]);
524
+ const runMutation = useCallback2(
525
+ async (scopeComputerSessionId, operation) => {
526
+ invalidateRefresh();
527
+ if (mutationRef.current.computerSessionId !== scopeComputerSessionId) {
528
+ mutationRef.current = {
529
+ computerSessionId: scopeComputerSessionId,
530
+ count: 0
531
+ };
532
+ }
533
+ mutationRef.current.count += 1;
534
+ setState(
535
+ (current) => current.computerSessionId === scopeComputerSessionId ? { ...current, mutating: true, error: null } : current
536
+ );
537
+ try {
538
+ return await operation();
539
+ } catch (cause) {
540
+ const error = cause instanceof Error ? cause : new Error(String(cause));
541
+ setState(
542
+ (current) => current.computerSessionId === scopeComputerSessionId ? { ...current, error } : current
543
+ );
544
+ throw error;
545
+ } finally {
546
+ if (mutationRef.current.computerSessionId === scopeComputerSessionId) {
547
+ mutationRef.current.count = Math.max(0, mutationRef.current.count - 1);
548
+ const mutating = mutationRef.current.count > 0;
549
+ setState(
550
+ (current) => current.computerSessionId === scopeComputerSessionId ? { ...current, mutating } : current
551
+ );
552
+ }
553
+ }
554
+ },
555
+ [invalidateRefresh]
556
+ );
557
+ const selectTarget = useCallback2(
558
+ async (targetId) => {
559
+ if (!computerSessionId) throw new Error("No desktop is selected.");
560
+ const target = targetsRef.current.targets.find((candidate) => candidate.id === targetId);
561
+ if (!target) throw new Error("The selected desktop target is no longer available.");
562
+ invalidateRefresh();
563
+ selectedTargetIdRef.current = target.id;
564
+ observationRef.current = { computerSessionId, observation: null };
565
+ setState(
566
+ (current) => current.computerSessionId === computerSessionId ? {
567
+ ...current,
568
+ selectedTargetId: target.id,
569
+ observation: null,
570
+ loading: true,
571
+ error: null
572
+ } : current
573
+ );
574
+ try {
575
+ const observation = await client.observeComputerTarget(
576
+ workspaceId,
577
+ computerSessionId,
578
+ target.id
579
+ );
580
+ if (selectedTargetIdRef.current !== target.id) return target;
581
+ observationRef.current = { computerSessionId, observation };
582
+ setState(
583
+ (current) => current.computerSessionId === computerSessionId && current.selectedTargetId === target.id ? { ...current, observation, loading: false, error: null } : current
584
+ );
585
+ return observation.target;
586
+ } catch (cause) {
587
+ const error = cause instanceof Error ? cause : new Error(String(cause));
588
+ setState(
589
+ (current) => current.computerSessionId === computerSessionId && current.selectedTargetId === target.id ? { ...current, loading: false, error } : current
590
+ );
591
+ throw error;
592
+ }
593
+ },
594
+ [client, computerSessionId, invalidateRefresh, workspaceId]
595
+ );
596
+ const dispatchAction = useCallback2(
597
+ async (action, operationId, frame) => {
598
+ if (!computerSessionId) throw new Error("No desktop is selected.");
599
+ if (frame && frame.computerSessionId !== computerSessionId) {
600
+ throw new Error("The displayed desktop frame belongs to another desktop session.");
601
+ }
602
+ const currentObservation = observationRef.current.computerSessionId === computerSessionId ? observationRef.current.observation : null;
603
+ const focusTarget = action.type === "focus" ? targetsRef.current.targets.find((candidate) => candidate.id === action.targetId) ?? null : null;
604
+ const frameTarget = frame ? targetsRef.current.targets.find((candidate) => candidate.id === frame.targetId) ?? (currentObservation?.target.id === frame.targetId ? currentObservation.target : null) : null;
605
+ const target = frameTarget ?? focusTarget ?? currentObservation?.target ?? null;
606
+ if (!target) throw new Error("The desktop target is not ready for input.");
607
+ if (frame && frame.targetId !== selectedTargetIdRef.current) {
608
+ throw new Error("The displayed desktop frame is no longer selected.");
609
+ }
610
+ if (action.type === "pointer") {
611
+ const expectedFrameId = frame?.frameId ?? currentObservation?.frameId ?? null;
612
+ if (!expectedFrameId || action.frameId !== expectedFrameId) {
613
+ throw new Error("Pointer input must reference the exact displayed desktop frame.");
614
+ }
615
+ }
616
+ if (action.type === "semantic" && !currentObservation) {
617
+ throw new Error("The desktop accessibility tree is not ready for input.");
618
+ }
619
+ return await runMutation(computerSessionId, async () => {
620
+ const receipt = await client.actInComputer(workspaceId, computerSessionId, {
621
+ operationId,
622
+ targetId: target.id,
623
+ expectedTargetGeneration: frame?.targetGeneration ?? target.targetGeneration,
624
+ expectedObservationId: action.type === "pointer" ? null : currentObservation?.observationId ?? null,
625
+ expectedFrameId: action.type === "pointer" ? action.frameId : null,
626
+ action
627
+ });
628
+ if (receipt.observation) {
629
+ const observation = receipt.observation;
630
+ observationRef.current = { computerSessionId, observation };
631
+ selectedTargetIdRef.current = observation.target.id;
632
+ setState(
633
+ (current) => current.computerSessionId === computerSessionId ? {
634
+ ...current,
635
+ observation,
636
+ selectedTargetId: observation.target.id,
637
+ targets: replaceTarget(current.targets, observation.target)
638
+ } : current
639
+ );
640
+ } else {
641
+ void refresh();
642
+ }
643
+ return receipt;
644
+ });
645
+ },
646
+ [client, computerSessionId, refresh, runMutation, workspaceId]
647
+ );
648
+ const act = useCallback2(
649
+ async (action, operationId = crypto.randomUUID()) => await dispatchAction(action, operationId, null),
650
+ [dispatchAction]
651
+ );
652
+ const actFromFrame = useCallback2(
653
+ async (action, frame, operationId = crypto.randomUUID()) => await dispatchAction(action, operationId, frame),
654
+ [dispatchAction]
655
+ );
656
+ const readClipboard = useCallback2(async () => {
657
+ if (!computerSessionId) throw new Error("No desktop is selected.");
658
+ return await client.readComputerClipboard(workspaceId, computerSessionId);
659
+ }, [client, computerSessionId, workspaceId]);
660
+ return {
661
+ session: visible.session,
662
+ targets: visible.targets,
663
+ selectedTarget: visible.targets.find((target) => target.id === visible.selectedTargetId) ?? null,
664
+ observation: visible.observation,
665
+ loading: visible.loading,
666
+ mutating: visible.mutating,
667
+ error: visible.error,
668
+ refresh,
669
+ readClipboard,
670
+ selectTarget,
671
+ act,
672
+ actFromFrame
673
+ };
674
+ }
675
+ function emptyState(computerSessionId, loading) {
676
+ return {
677
+ computerSessionId,
678
+ session: null,
679
+ targets: [],
680
+ selectedTargetId: null,
681
+ observation: null,
682
+ loading,
683
+ mutating: false,
684
+ error: null
685
+ };
686
+ }
687
+ function chooseTarget(targets, preferredId, platform) {
688
+ const preferred = targets.find((target) => target.id === preferredId);
689
+ if (preferred) return preferred;
690
+ if (platform === "linux") {
691
+ return targets.find((target) => target.focused && target.kind === "screen") ?? targets.find((target) => target.kind === "screen") ?? targets.find((target) => target.focused && target.kind === "window") ?? targets.find((target) => target.kind === "window") ?? targets[0] ?? null;
692
+ }
693
+ return targets.find((target) => target.focused && target.kind === "window") ?? targets.find((target) => target.kind === "window") ?? targets.find((target) => target.kind === "screen") ?? targets.find((target) => target.focused && target.kind === "app") ?? targets.find((target) => target.kind === "app") ?? targets[0] ?? null;
694
+ }
695
+ function sortComputerTargets(targets) {
696
+ const rank = { window: 0, app: 1, screen: 2 };
697
+ return [...targets].sort(
698
+ (left, right) => Number(right.focused) - Number(left.focused) || rank[left.kind] - rank[right.kind] || left.title.localeCompare(right.title) || left.id.localeCompare(right.id)
699
+ );
700
+ }
701
+ function replaceTarget(targets, target) {
702
+ return sortComputerTargets([
703
+ ...targets.filter((candidate) => candidate.id !== target.id),
704
+ target
705
+ ]);
706
+ }
707
+
708
+ // src/hooks/use-computer-sessions.ts
709
+ import { useCallback as useCallback3, useEffect as useEffect3, useMemo, useRef as useRef3, useState as useState3 } from "react";
710
+ function useComputerSessions(options = {}) {
711
+ const {
712
+ client,
713
+ workspaceId,
714
+ registerSessionReconciler,
715
+ workspaceInteractionEvent,
716
+ workspaceInteractionConnectionState
717
+ } = useEmbeddedComputerInteraction(options);
718
+ const enabled = options.enabled ?? true;
719
+ const [state, setState] = useState3(() => emptyState2(workspaceId, enabled));
720
+ const requestRef = useRef3({
721
+ id: 0,
722
+ controller: null
723
+ });
724
+ const mountedRef = useRef3(true);
725
+ const visible = state.workspaceId === workspaceId ? state : emptyState2(workspaceId, enabled);
726
+ const cancelLoad = useCallback3(() => {
727
+ const id = requestRef.current.id + 1;
728
+ requestRef.current.controller?.abort();
729
+ requestRef.current = { id, controller: null };
730
+ }, []);
731
+ const load = useCallback3(
732
+ async (foreground) => {
733
+ if (!enabled) return;
734
+ const id = requestRef.current.id + 1;
735
+ requestRef.current.controller?.abort();
736
+ const controller = new AbortController();
737
+ requestRef.current = { id, controller };
738
+ setState((current) => ({
739
+ ...current.workspaceId === workspaceId ? current : emptyState2(workspaceId, true),
740
+ active: true,
741
+ loading: foreground,
742
+ refreshing: !foreground,
743
+ error: foreground || current.workspaceId !== workspaceId ? null : current.error
744
+ }));
745
+ try {
746
+ const response = await client.listComputerSessions(workspaceId, {
747
+ signal: controller.signal
748
+ });
749
+ if (!mountedRef.current || requestRef.current.id !== id) return;
750
+ setState({
751
+ workspaceId,
752
+ active: true,
753
+ revision: response.revision,
754
+ sessions: sortComputerSessions(response.sessions),
755
+ loading: false,
756
+ refreshing: false,
757
+ error: null
758
+ });
759
+ } catch (cause) {
760
+ if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id)
761
+ return;
762
+ setState((current) => ({
763
+ ...current.workspaceId === workspaceId ? current : emptyState2(workspaceId, true),
764
+ loading: false,
765
+ refreshing: false,
766
+ error: cause instanceof Error ? cause : new Error(String(cause))
767
+ }));
768
+ } finally {
769
+ if (requestRef.current.id === id) requestRef.current = { id, controller: null };
770
+ }
771
+ },
772
+ [client, enabled, workspaceId]
773
+ );
774
+ const refresh = useCallback3(async () => await load(false), [load]);
775
+ useEffect3(() => {
776
+ mountedRef.current = true;
777
+ if (!enabled) {
778
+ cancelLoad();
779
+ setState(emptyState2(workspaceId, false));
780
+ return;
781
+ }
782
+ void load(true);
783
+ return () => {
784
+ mountedRef.current = false;
785
+ cancelLoad();
786
+ };
787
+ }, [cancelLoad, enabled, load, workspaceId]);
788
+ useInteractionInvalidation({
789
+ workspaceId,
790
+ key: workspaceId,
791
+ enabled,
792
+ event: workspaceInteractionEvent,
793
+ connectionState: workspaceInteractionConnectionState,
794
+ knownRevision: visible.revision,
795
+ refresh,
796
+ fallbackPollIntervalMs: options.pollIntervalMs
797
+ });
798
+ useEffect3(() => {
799
+ if (!enabled || !options.sessionId) return;
800
+ return registerSessionReconciler(
801
+ options.sessionId,
802
+ `computer-sessions:${workspaceId}`,
803
+ refresh
804
+ );
805
+ }, [enabled, options.sessionId, refresh, registerSessionReconciler, workspaceId]);
806
+ const create = useCallback3(
807
+ async (request) => {
808
+ const response = await client.createComputerSession(workspaceId, {
809
+ ...request,
810
+ operationId: request.operationId ?? crypto.randomUUID()
811
+ });
812
+ setState((current) => mergeSession(current, workspaceId, response.session));
813
+ return response;
814
+ },
815
+ [client, workspaceId]
816
+ );
817
+ const end = useCallback3(
818
+ async (computerSessionId, operationId = crypto.randomUUID()) => {
819
+ const response = await client.endComputerSession(workspaceId, computerSessionId, {
820
+ operationId
821
+ });
822
+ setState((current) => mergeSession(current, workspaceId, response.session));
823
+ return response;
824
+ },
825
+ [client, workspaceId]
826
+ );
827
+ const relevantSessions = useMemo(
828
+ () => options.sessionId ? visible.sessions.filter(
829
+ (session) => session.associations.some((association) => association.sessionId === options.sessionId)
830
+ ) : [],
831
+ [options.sessionId, visible.sessions]
832
+ );
833
+ return {
834
+ revision: visible.revision,
835
+ sessions: visible.sessions,
836
+ relevantSessions,
837
+ // A disabled registry is deliberately empty. When its surface becomes
838
+ // visible again, expose that transition as loading in the same render so
839
+ // consumers cannot mistake the stale empty snapshot for a completed list.
840
+ loading: enabled ? !visible.active || visible.loading : false,
841
+ refreshing: visible.refreshing,
842
+ error: visible.error,
843
+ refresh,
844
+ create,
845
+ end
846
+ };
847
+ }
848
+ function emptyState2(workspaceId, loading) {
849
+ return {
850
+ workspaceId,
851
+ active: loading,
852
+ revision: 0,
853
+ sessions: [],
854
+ loading,
855
+ refreshing: false,
856
+ error: null
857
+ };
858
+ }
859
+ function sortComputerSessions(sessions) {
860
+ return [...sessions].sort((left, right) => {
861
+ const live = Number(isLiveComputer(right)) - Number(isLiveComputer(left));
862
+ return live || timestamp(right.lastUsedAt) - timestamp(left.lastUsedAt) || left.id.localeCompare(right.id);
863
+ });
864
+ }
865
+ function isLiveComputer(session) {
866
+ return !["ending", "ended", "failed", "lost"].includes(session.lifecycle);
867
+ }
868
+ function timestamp(value) {
869
+ const parsed = Date.parse(value);
870
+ return Number.isFinite(parsed) ? parsed : 0;
871
+ }
872
+ function mergeSession(current, workspaceId, session) {
873
+ if (current.workspaceId !== workspaceId) return current;
874
+ const sessions = current.sessions.filter((candidate) => candidate.id !== session.id);
875
+ sessions.push(session);
876
+ return { ...current, sessions: sortComputerSessions(sessions), error: null };
877
+ }
878
+
879
+ // src/components/computer-viewer.tsx
880
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
881
+ function ComputerViewer({
882
+ sessionId,
883
+ enabled = true,
884
+ className,
885
+ onNotify,
886
+ webSocketFactory,
887
+ renderEmpty,
888
+ requestedComputerSessionId,
889
+ requestedComputerRequestId,
890
+ initialComputerSessionId,
891
+ onComputerSessionIdChange,
892
+ ...override
893
+ }) {
894
+ const registry = useComputerSessions({ ...override, sessionId, enabled });
895
+ const createSession = registry.create;
896
+ const refreshRegistry = registry.refresh;
897
+ const liveRelevant = useMemo2(
898
+ () => registry.relevantSessions.filter((session) => isLiveComputer2(session)),
899
+ [registry.relevantSessions]
900
+ );
901
+ const connectedPlacementLoss = useMemo2(
902
+ () => registry.relevantSessions.find(
903
+ (session) => session.lifecycle === "lost" && session.failureCode === "source_placement_changed" && session.placement.kind === "connected_machine"
904
+ ) ?? null,
905
+ [registry.relevantSessions]
906
+ );
907
+ const recentRelevantFailure = useMemo2(
908
+ () => liveRelevant.length === 0 ? registry.relevantSessions.find(
909
+ (session) => ["failed", "lost"].includes(session.lifecycle) && !(session.failureCode === "source_placement_changed" && session.placement.kind === "connected_machine")
910
+ ) ?? null : null,
911
+ [liveRelevant.length, registry.relevantSessions]
912
+ );
913
+ const liveSessions = useMemo2(() => {
914
+ const sessions = registry.sessions.filter((session) => isLiveComputer2(session));
915
+ return recentRelevantFailure ? [...sessions, recentRelevantFailure] : sessions;
916
+ }, [recentRelevantFailure, registry.sessions]);
917
+ const relevant = useMemo2(
918
+ () => liveRelevant.length > 0 ? liveRelevant : recentRelevantFailure ? [recentRelevantFailure] : [],
919
+ [liveRelevant, recentRelevantFailure]
920
+ );
921
+ const [selection, setSelection] = useState4(
922
+ () => initialComputerSessionId ? { sessionId: initialComputerSessionId, pinned: true } : null
923
+ );
924
+ const selectComputerSession = useCallback4(
925
+ (next) => {
926
+ setSelection(next);
927
+ onComputerSessionIdChange?.(next?.sessionId ?? null);
928
+ },
929
+ [onComputerSessionIdChange]
930
+ );
931
+ const interventions = useInteractionInterventions({
932
+ ...override,
933
+ enabled,
934
+ resourceKind: "computer_session"
935
+ });
936
+ const [creating, setCreating] = useState4(false);
937
+ const [createError, setCreateError] = useState4(null);
938
+ const [showControls, setShowControls] = useState4(false);
939
+ const previousSessionIdRef = useRef4(sessionId);
940
+ const handledRequestRef = useRef4(null);
941
+ const seenInterventionIdsRef = useRef4(/* @__PURE__ */ new Set());
942
+ const createInFlightRef = useRef4(false);
943
+ const autoCreateSessionRef = useRef4(null);
944
+ const emptyCatalogConfirmationRef = useRef4(null);
945
+ const endedGenerationLossRef = useRef4(/* @__PURE__ */ new Set());
946
+ const recreatedPlacementLossRef = useRef4(/* @__PURE__ */ new Set());
947
+ const [suppressGenericCreate, setSuppressGenericCreate] = useState4(false);
948
+ const endStaleComputer = registry.end;
949
+ useEffect4(() => {
950
+ if (!enabled) return;
951
+ for (const session of registry.sessions) {
952
+ if (!isAttachedChromeGenerationLoss(session)) continue;
953
+ setSuppressGenericCreate(true);
954
+ if (endedGenerationLossRef.current.has(session.id)) continue;
955
+ endedGenerationLossRef.current.add(session.id);
956
+ void endStaleComputer(session.id).catch(() => {
957
+ endedGenerationLossRef.current.delete(session.id);
958
+ });
959
+ }
960
+ }, [enabled, endStaleComputer, registry.sessions]);
961
+ const notifyError = useCallback4(
962
+ (cause, fallback) => {
963
+ onNotify?.({
964
+ kind: "error",
965
+ message: cause instanceof Error ? cause.message : fallback
966
+ });
967
+ },
968
+ [onNotify]
969
+ );
970
+ useEffect4(() => {
971
+ if (previousSessionIdRef.current === sessionId) return;
972
+ previousSessionIdRef.current = sessionId;
973
+ handledRequestRef.current = null;
974
+ autoCreateSessionRef.current = null;
975
+ emptyCatalogConfirmationRef.current = null;
976
+ recreatedPlacementLossRef.current.clear();
977
+ setSuppressGenericCreate(false);
978
+ setCreateError(null);
979
+ setSelection(
980
+ initialComputerSessionId ? { sessionId: initialComputerSessionId, pinned: true } : null
981
+ );
982
+ }, [initialComputerSessionId, sessionId]);
983
+ useEffect4(() => {
984
+ if (!enabled || registry.loading) return;
985
+ const requested = requestedComputerSessionId ? liveSessions.find((session) => session.id === requestedComputerSessionId) ?? null : null;
986
+ const requestKey = requested ? `${requestedComputerRequestId ?? "default"}:${requested.id}` : null;
987
+ if (requested && requestKey && handledRequestRef.current !== requestKey) {
988
+ handledRequestRef.current = requestKey;
989
+ selectComputerSession({ sessionId: requested.id, pinned: true });
990
+ return;
991
+ }
992
+ const selectedStillLive = liveSessions.some((session) => session.id === selection?.sessionId);
993
+ if (selection?.pinned && selectedStillLive) return;
994
+ const preferred = relevant[0] ?? null;
995
+ if (!preferred) {
996
+ if (selection) selectComputerSession(null);
997
+ return;
998
+ }
999
+ if (!selectedStillLive || !selection?.pinned) {
1000
+ if (selection?.sessionId !== preferred.id || selection.pinned) {
1001
+ selectComputerSession({ sessionId: preferred.id, pinned: false });
1002
+ }
1003
+ }
1004
+ }, [
1005
+ liveSessions,
1006
+ enabled,
1007
+ registry.loading,
1008
+ relevant,
1009
+ requestedComputerRequestId,
1010
+ requestedComputerSessionId,
1011
+ selectComputerSession,
1012
+ selection
1013
+ ]);
1014
+ const selectedRegistrySession = useMemo2(
1015
+ () => liveSessions.find((session) => session.id === selection?.sessionId) ?? null,
1016
+ [liveSessions, selection?.sessionId]
1017
+ );
1018
+ const interventionCounts = useMemo2(
1019
+ () => countInterventions(interventions.interventions),
1020
+ [interventions.interventions]
1021
+ );
1022
+ const selectedInterventions = useMemo2(
1023
+ () => interventions.interventions.filter(
1024
+ (intervention) => intervention.resourceId === selection?.sessionId
1025
+ ),
1026
+ [interventions.interventions, selection?.sessionId]
1027
+ );
1028
+ const controllerReady = selectedRegistrySession?.lifecycle === "active";
1029
+ const computer = useComputerSession({
1030
+ ...override,
1031
+ computerSessionId: selection?.sessionId ?? null,
1032
+ enabled: enabled && selection !== null && controllerReady
1033
+ });
1034
+ useEffect4(() => {
1035
+ if (!isSourcePlacementChangedError(computer.error, "computer_session")) return;
1036
+ void refreshRegistry();
1037
+ }, [computer.error, refreshRegistry]);
1038
+ const act = computer.act;
1039
+ const actFromFrame = computer.actFromFrame;
1040
+ const refreshComputer = computer.refresh;
1041
+ const frames = useComputerFrameStream({
1042
+ ...override,
1043
+ computerSessionId: selection?.sessionId ?? null,
1044
+ targetId: computer.selectedTarget?.id ?? null,
1045
+ enabled: enabled && selection !== null && controllerReady && computer.selectedTarget !== null && isComputerFrameTarget(computer.selectedTarget),
1046
+ stream: {
1047
+ format: "jpeg",
1048
+ quality: 78,
1049
+ maxWidth: 1920,
1050
+ maxHeight: 1200,
1051
+ everyNthFrame: 1
1052
+ },
1053
+ ...webSocketFactory ? { webSocketFactory } : {}
1054
+ });
1055
+ const displayedFrame = frames.frame && frames.frame.computerSessionId === selection?.sessionId && frames.frame.targetId === computer.selectedTarget?.id ? frames.frame : null;
1056
+ const rfbStream = frames.attachment?.stream.kind === "direct_rfb" ? frames.attachment.stream : null;
1057
+ const rfbCapability = useMemo2(() => {
1058
+ if (!rfbStream || !frames.attachment) return null;
1059
+ const bounds = computer.selectedTarget?.bounds;
1060
+ return {
1061
+ transport: "vnc-ws",
1062
+ client: "novnc",
1063
+ mode: "interactive",
1064
+ url: rfbStream.url,
1065
+ token: null,
1066
+ expiresAt: frames.attachment.expiresAt,
1067
+ resolution: [bounds?.width ?? 1440, bounds?.height ?? 900],
1068
+ unredacted: true,
1069
+ requiresAcknowledgment: false,
1070
+ acknowledged: true,
1071
+ shared: false,
1072
+ sharedSessionIds: [],
1073
+ reason: null
1074
+ };
1075
+ }, [computer.selectedTarget?.bounds, frames.attachment, rfbStream]);
1076
+ const machineLocked = selectedRegistrySession?.failureCode === "machine_locked";
1077
+ const currentIds = useMemo2(() => new Set(relevant.map((session) => session.id)), [relevant]);
1078
+ useEffect4(() => {
1079
+ for (const intervention of interventions.interventions) {
1080
+ if (seenInterventionIdsRef.current.has(intervention.id)) continue;
1081
+ seenInterventionIdsRef.current.add(intervention.id);
1082
+ onNotify?.({
1083
+ kind: "info",
1084
+ message: `${interventionTitle(intervention)}: ${intervention.reason}`
1085
+ });
1086
+ }
1087
+ }, [interventions.interventions, onNotify]);
1088
+ const resolveIntervention = useCallback4(
1089
+ (intervention, outcome) => {
1090
+ void interventions.resolve(intervention.id, {
1091
+ expectedVersion: intervention.version,
1092
+ outcome
1093
+ }).then(() => {
1094
+ onNotify?.({
1095
+ kind: "info",
1096
+ message: outcome === "completed" ? "Agent notified. Continuing work." : "Request cancelled."
1097
+ });
1098
+ }).catch((cause) => notifyError(cause, "Could not update the desktop request."));
1099
+ },
1100
+ [interventions, notifyError, onNotify]
1101
+ );
1102
+ const createComputer = useCallback4(() => {
1103
+ if (createInFlightRef.current) return;
1104
+ createInFlightRef.current = true;
1105
+ setCreateError(null);
1106
+ setCreating(true);
1107
+ void createSession({ sessionId, name: "Desktop" }).then((response) => {
1108
+ selectComputerSession({ sessionId: response.session.id, pinned: false });
1109
+ }).catch((cause) => {
1110
+ const error = cause instanceof Error ? cause : new Error("Could not open a desktop.");
1111
+ setCreateError(error);
1112
+ notifyError(error, "Could not open a desktop.");
1113
+ }).finally(() => {
1114
+ createInFlightRef.current = false;
1115
+ setCreating(false);
1116
+ });
1117
+ }, [createSession, notifyError, selectComputerSession, sessionId]);
1118
+ useEffect4(() => {
1119
+ if (!enabled || registry.loading || registry.refreshing || liveRelevant.length > 0 || !connectedPlacementLoss || recreatedPlacementLossRef.current.has(connectedPlacementLoss.id)) {
1120
+ return;
1121
+ }
1122
+ recreatedPlacementLossRef.current.add(connectedPlacementLoss.id);
1123
+ autoCreateSessionRef.current = sessionId;
1124
+ emptyCatalogConfirmationRef.current = null;
1125
+ createComputer();
1126
+ }, [
1127
+ connectedPlacementLoss,
1128
+ createComputer,
1129
+ enabled,
1130
+ liveRelevant.length,
1131
+ registry.loading,
1132
+ registry.refreshing,
1133
+ sessionId
1134
+ ]);
1135
+ useEffect4(() => {
1136
+ if (relevant.length > 0) {
1137
+ autoCreateSessionRef.current = sessionId;
1138
+ emptyCatalogConfirmationRef.current = null;
1139
+ }
1140
+ }, [relevant.length, sessionId]);
1141
+ useEffect4(() => {
1142
+ if (!enabled || registry.loading || registry.refreshing || registry.error || relevant.length > 0 || suppressGenericCreate || autoCreateSessionRef.current === sessionId) {
1143
+ return;
1144
+ }
1145
+ if (emptyCatalogConfirmationRef.current !== sessionId) {
1146
+ emptyCatalogConfirmationRef.current = sessionId;
1147
+ void refreshRegistry();
1148
+ return;
1149
+ }
1150
+ autoCreateSessionRef.current = sessionId;
1151
+ createComputer();
1152
+ }, [
1153
+ createComputer,
1154
+ enabled,
1155
+ refreshRegistry,
1156
+ registry.error,
1157
+ registry.loading,
1158
+ registry.refreshing,
1159
+ relevant.length,
1160
+ sessionId,
1161
+ suppressGenericCreate
1162
+ ]);
1163
+ const perform = useCallback4(
1164
+ async (action, frame) => {
1165
+ if (action.type === "pointer") {
1166
+ if (!frame) throw new Error("Desktop view is not ready for pointer input.");
1167
+ await actFromFrame(action, frame);
1168
+ } else {
1169
+ await act(action);
1170
+ }
1171
+ },
1172
+ [act, actFromFrame]
1173
+ );
1174
+ const copyFromRfb = useCallback4(
1175
+ (event) => {
1176
+ if (!rfbStream || computer.session?.capabilities?.clipboard !== true) return;
1177
+ event.preventDefault();
1178
+ event.stopPropagation();
1179
+ void perform({ type: "clipboard", operation: "copy" }, null).then(() => computer.readClipboard()).then(async (clipboard) => {
1180
+ if (clipboard.text && !await copyTextToClipboard(clipboard.text)) {
1181
+ throw new Error("Desktop text could not be copied to the local clipboard");
1182
+ }
1183
+ }).catch((cause) => notifyError(cause, "Could not copy from the desktop."));
1184
+ },
1185
+ [computer, notifyError, perform, rfbStream]
1186
+ );
1187
+ const pasteIntoRfb = useCallback4(
1188
+ (text) => {
1189
+ if (!rfbStream || computer.session?.capabilities?.clipboard !== true) return false;
1190
+ void (async () => {
1191
+ await perform({ type: "clipboard", operation: "write", text }, null);
1192
+ assertExactComputerClipboard(await computer.readClipboard(), text);
1193
+ await perform({ type: "clipboard", operation: "paste" }, null);
1194
+ })().catch((cause) => notifyError(cause, "Could not paste into the desktop."));
1195
+ return true;
1196
+ },
1197
+ [computer, notifyError, perform, rfbStream]
1198
+ );
1199
+ const hideGenericCreate = suppressGenericCreate && liveRelevant.length === 0;
1200
+ const generationLossSelected = selectedRegistrySession !== null && isAttachedChromeGenerationLoss(selectedRegistrySession);
1201
+ const generationLossFrames = isAttachedChromeGenerationLossError(frames.error);
1202
+ if (!enabled) return null;
1203
+ if (registry.loading && liveSessions.length === 0) {
1204
+ return /* @__PURE__ */ jsx(
1205
+ ComputerNotice,
1206
+ {
1207
+ icon: /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-4 animate-spin" }),
1208
+ text: "Finding workspace desktops\u2026",
1209
+ ...className ? { className } : {}
1210
+ }
1211
+ );
1212
+ }
1213
+ if (liveSessions.length === 0) {
1214
+ if (hideGenericCreate) {
1215
+ return /* @__PURE__ */ jsx("div", { className: cn("grid h-full place-items-center bg-og-bg p-6", className), children: /* @__PURE__ */ jsxs("div", { className: "max-w-sm text-center", children: [
1216
+ /* @__PURE__ */ jsx(CircleAlertIcon, { className: "mx-auto size-5 text-og-status-error" }),
1217
+ /* @__PURE__ */ jsx("p", { className: "mt-3 text-og-menu font-medium text-og-fg", children: "Chrome reconnected\u2014open a fresh browser/desktop." }),
1218
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-og-control leading-5 text-og-muted", children: "This Chrome profile is live again, but the previous desktop cannot move to the new connection. Use Browser \u2192 New browser \u2192 Connected Chrome." })
1219
+ ] }) });
1220
+ }
1221
+ if (renderEmpty) return renderEmpty(createComputer, creating);
1222
+ const error = createError ?? registry.error;
1223
+ return /* @__PURE__ */ jsx("div", { className: cn("grid h-full place-items-center bg-og-bg p-6", className), children: /* @__PURE__ */ jsxs("div", { className: "max-w-sm text-center", children: [
1224
+ /* @__PURE__ */ jsx("span", { className: "mx-auto grid size-10 place-items-center rounded-og-lg border border-og-border bg-og-surface-1 text-og-muted", children: error ? /* @__PURE__ */ jsx(CircleAlertIcon, { className: "size-5 text-og-status-error" }) : /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-5 animate-spin" }) }),
1225
+ /* @__PURE__ */ jsx("p", { className: "mt-3 text-og-menu font-medium text-og-fg", children: error ? "Desktop didn\u2019t open" : "Opening desktop\u2026" }),
1226
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-og-control leading-5 text-og-muted", children: error?.message ?? "Preparing this agent\u2019s desktop. It will appear here when ready." }),
1227
+ error ? /* @__PURE__ */ jsxs(
1228
+ "button",
1229
+ {
1230
+ type: "button",
1231
+ onClick: createError ? createComputer : () => void refreshRegistry(),
1232
+ disabled: creating || registry.refreshing,
1233
+ className: "mt-4 inline-flex h-8 items-center gap-1.5 rounded-og-sm bg-og-accent-deep px-3 text-og-control font-medium text-og-accent-fg transition hover:brightness-110 disabled:opacity-50",
1234
+ children: [
1235
+ creating || registry.refreshing ? /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-3.5 animate-spin" }) : /* @__PURE__ */ jsx(RotateCcwIcon, { className: "size-3.5" }),
1236
+ "Try again"
1237
+ ]
1238
+ }
1239
+ ) : null
1240
+ ] }) });
1241
+ }
1242
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex h-full min-h-0 flex-col overflow-hidden bg-og-bg", className), children: [
1243
+ /* @__PURE__ */ jsx(
1244
+ ComputerToolbar,
1245
+ {
1246
+ sessions: liveSessions,
1247
+ relevantSessionIds: currentIds,
1248
+ selectedSessionId: selection?.sessionId ?? null,
1249
+ creating,
1250
+ refreshing: registry.refreshing,
1251
+ interventionCounts,
1252
+ onSelect: (computerSessionId) => selectComputerSession({ sessionId: computerSessionId, pinned: true }),
1253
+ onFollow: () => {
1254
+ if (relevant[0]) selectComputerSession({ sessionId: relevant[0].id, pinned: false });
1255
+ },
1256
+ onCreate: hideGenericCreate ? void 0 : createComputer,
1257
+ onRefresh: () => void Promise.all([refreshRegistry(), refreshComputer()])
1258
+ }
1259
+ ),
1260
+ /* @__PURE__ */ jsx(
1261
+ InteractionInterventionBanner,
1262
+ {
1263
+ interventions: selectedInterventions,
1264
+ activeTargetId: computer.selectedTarget?.id ?? null,
1265
+ mutating: interventions.mutating,
1266
+ onOpen: (intervention) => void computer.selectTarget(intervention.targetId).catch((cause) => notifyError(cause, "Could not open the requested desktop view.")),
1267
+ onResolve: resolveIntervention
1268
+ }
1269
+ ),
1270
+ !selectedRegistrySession ? /* @__PURE__ */ jsx(
1271
+ ComputerUnselectedPanel,
1272
+ {
1273
+ peerCount: liveSessions.length,
1274
+ creating,
1275
+ error: createError,
1276
+ generationLoss: suppressGenericCreate,
1277
+ onCreate: createComputer
1278
+ }
1279
+ ) : !controllerReady ? /* @__PURE__ */ jsx(
1280
+ ComputerLifecyclePanel,
1281
+ {
1282
+ session: selectedRegistrySession,
1283
+ creating,
1284
+ onRefresh: registry.refresh,
1285
+ onRetry: createComputer
1286
+ }
1287
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
1288
+ /* @__PURE__ */ jsx(
1289
+ ComputerTargetRail,
1290
+ {
1291
+ targets: computer.targets,
1292
+ selectedTargetId: computer.selectedTarget?.id ?? null,
1293
+ loading: computer.loading,
1294
+ onSelect: (targetId) => void computer.selectTarget(targetId).catch((cause) => notifyError(cause, "Could not switch desktop views."))
1295
+ }
1296
+ ),
1297
+ /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1", children: [
1298
+ rfbStream ? /* @__PURE__ */ jsx("div", { className: "relative min-h-0 flex-1 bg-black", onCopyCapture: copyFromRfb, children: /* @__PURE__ */ jsx(
1299
+ DesktopViewer,
1300
+ {
1301
+ capability: rfbCapability,
1302
+ interactive: true,
1303
+ showControlToggle: false,
1304
+ webSocketProtocols: rfbStream.protocols,
1305
+ onPasteText: pasteIntoRfb,
1306
+ targetPlatform: computer.session?.platform ?? null,
1307
+ className: "h-full"
1308
+ }
1309
+ ) }) : /* @__PURE__ */ jsx(
1310
+ ComputerViewport,
1311
+ {
1312
+ frame: machineLocked ? null : displayedFrame,
1313
+ observation: computer.observation,
1314
+ target: computer.selectedTarget,
1315
+ machineLocked,
1316
+ connectionState: frames.state,
1317
+ connectionError: frames.error ?? computer.error,
1318
+ mutating: computer.mutating,
1319
+ backgroundActions: computer.session?.capabilities?.backgroundActions === true,
1320
+ clipboardEnabled: computer.session?.capabilities?.clipboard === true,
1321
+ onAction: perform,
1322
+ onReadClipboard: computer.readClipboard,
1323
+ onReconnect: generationLossSelected || generationLossFrames ? () => void 0 : frames.reconnect,
1324
+ onError: (cause) => notifyError(cause, "Desktop input failed.")
1325
+ }
1326
+ ),
1327
+ showControls ? /* @__PURE__ */ jsx(
1328
+ ComputerSemanticPanel,
1329
+ {
1330
+ observation: computer.observation,
1331
+ mutating: computer.mutating,
1332
+ onAction: (action) => void perform(action, null).catch(
1333
+ (cause) => notifyError(cause, "Desktop action failed.")
1334
+ )
1335
+ }
1336
+ ) : null
1337
+ ] }),
1338
+ /* @__PURE__ */ jsx(
1339
+ ComputerStatusBar,
1340
+ {
1341
+ session: computer.session,
1342
+ target: computer.selectedTarget,
1343
+ connectionState: frames.state,
1344
+ refreshing: registry.refreshing,
1345
+ showControls,
1346
+ controlCount: semanticNodes(computer.observation).length,
1347
+ onToggleControls: () => setShowControls((current) => !current)
1348
+ }
1349
+ )
1350
+ ] })
1351
+ ] });
1352
+ }
1353
+ function ComputerUnselectedPanel(props) {
1354
+ 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: [
1355
+ /* @__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: props.error || props.generationLoss ? /* @__PURE__ */ jsx(CircleAlertIcon, { className: "size-4.5 text-og-status-error" }) : /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-4.5 animate-spin" }) }),
1356
+ /* @__PURE__ */ jsx("p", { className: "mt-3 text-og-menu font-medium text-og-fg", children: props.generationLoss ? "Chrome reconnected\u2014open a fresh browser/desktop." : props.error ? "Desktop didn\u2019t open" : "Opening desktop\u2026" }),
1357
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-og-control leading-5 text-og-muted", children: props.generationLoss ? "This Chrome profile is live again, but the previous desktop cannot move to the new connection. Use Browser \u2192 New browser \u2192 Connected Chrome." : props.error?.message ?? (props.peerCount === 1 ? "Preparing this agent\u2019s desktop. One peer desktop remains available from the menu." : `Preparing this agent\u2019s desktop. ${props.peerCount} peer desktops remain available from the menu.`) }),
1358
+ props.error && !props.generationLoss ? /* @__PURE__ */ jsxs(
1359
+ "button",
1360
+ {
1361
+ type: "button",
1362
+ disabled: props.creating,
1363
+ onClick: props.onCreate,
1364
+ 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",
1365
+ children: [
1366
+ props.creating ? /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-3.5 animate-spin" }) : /* @__PURE__ */ jsx(RotateCcwIcon, { className: "size-3.5" }),
1367
+ "Try again"
1368
+ ]
1369
+ }
1370
+ ) : null
1371
+ ] }) });
1372
+ }
1373
+ function ComputerToolbar(props) {
1374
+ const detailsRef = useRef4(null);
1375
+ const selected = props.sessions.find((session) => session.id === props.selectedSessionId);
1376
+ const current = props.sessions.filter((session) => props.relevantSessionIds.has(session.id));
1377
+ const others = props.sessions.filter((session) => !props.relevantSessionIds.has(session.id));
1378
+ const choose = (id) => {
1379
+ props.onSelect(id);
1380
+ detailsRef.current?.removeAttribute("open");
1381
+ };
1382
+ 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: [
1383
+ /* @__PURE__ */ jsxs("details", { ref: detailsRef, className: "relative min-w-0", children: [
1384
+ /* @__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: [
1385
+ /* @__PURE__ */ jsx(MonitorIcon, { className: "size-3.5 shrink-0 text-og-muted" }),
1386
+ /* @__PURE__ */ jsx("span", { className: "truncate font-medium", children: selected?.name === "Computer" ? "Desktop" : selected?.name ?? "Desktop" }),
1387
+ (props.interventionCounts.get(selected?.id ?? "") ?? 0) > 0 ? /* @__PURE__ */ jsx("span", { className: "size-1.5 shrink-0 rounded-full bg-og-status-waiting" }) : null,
1388
+ /* @__PURE__ */ jsx(ChevronDownIcon, { className: "size-3 shrink-0 text-og-subtle" })
1389
+ ] }),
1390
+ /* @__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: [
1391
+ /* @__PURE__ */ jsx(
1392
+ ComputerSessionGroup,
1393
+ {
1394
+ label: "Current agent",
1395
+ sessions: current,
1396
+ selectedId: props.selectedSessionId,
1397
+ interventionCounts: props.interventionCounts,
1398
+ onSelect: choose
1399
+ }
1400
+ ),
1401
+ /* @__PURE__ */ jsx(
1402
+ ComputerSessionGroup,
1403
+ {
1404
+ label: "Other agents",
1405
+ sessions: others,
1406
+ selectedId: props.selectedSessionId,
1407
+ interventionCounts: props.interventionCounts,
1408
+ onSelect: choose
1409
+ }
1410
+ ),
1411
+ /* @__PURE__ */ jsxs("div", { className: "mt-1 flex gap-1 border-t border-og-border pt-1", children: [
1412
+ current.length > 0 ? /* @__PURE__ */ jsx(MenuButton, { onClick: props.onFollow, children: "Follow agent" }) : null,
1413
+ props.onCreate ? /* @__PURE__ */ jsxs(MenuButton, { onClick: props.onCreate, disabled: props.creating, children: [
1414
+ /* @__PURE__ */ jsx(PlusIcon, { className: "size-3.5" }),
1415
+ " New desktop"
1416
+ ] }) : null
1417
+ ] })
1418
+ ] })
1419
+ ] }),
1420
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-og-xs text-og-subtle", children: selected ? `${platformLabel(selected)} \xB7 ${placementLabel(selected)}` : "" }),
1421
+ /* @__PURE__ */ jsx(
1422
+ "button",
1423
+ {
1424
+ type: "button",
1425
+ onClick: props.onRefresh,
1426
+ className: "grid size-7 place-items-center rounded-og-sm text-og-muted transition hover:bg-og-surface-2 hover:text-og-fg",
1427
+ "aria-label": "Refresh desktops",
1428
+ children: /* @__PURE__ */ jsx(RefreshCwIcon, { className: cn("size-3.5", props.refreshing && "animate-spin") })
1429
+ }
1430
+ ),
1431
+ props.onCreate ? /* @__PURE__ */ jsx(
1432
+ "button",
1433
+ {
1434
+ type: "button",
1435
+ onClick: props.onCreate,
1436
+ disabled: props.creating,
1437
+ 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",
1438
+ "aria-label": "Open a new desktop",
1439
+ children: props.creating ? /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-3.5 animate-spin" }) : /* @__PURE__ */ jsx(PlusIcon, { className: "size-3.5" })
1440
+ }
1441
+ ) : null
1442
+ ] });
1443
+ }
1444
+ function ComputerSessionGroup(props) {
1445
+ if (props.sessions.length === 0) return null;
1446
+ return /* @__PURE__ */ jsxs("div", { className: "py-1", children: [
1447
+ /* @__PURE__ */ jsx("p", { className: "px-2 pb-1 text-[10px] font-medium uppercase tracking-[0.12em] text-og-subtle", children: props.label }),
1448
+ props.sessions.map((session) => {
1449
+ const interventionCount = props.interventionCounts.get(session.id) ?? 0;
1450
+ return /* @__PURE__ */ jsxs(
1451
+ "button",
1452
+ {
1453
+ type: "button",
1454
+ onClick: () => props.onSelect(session.id),
1455
+ className: cn(
1456
+ "flex w-full items-center gap-2 rounded-og-sm px-2 py-1.5 text-left transition hover:bg-og-surface-2",
1457
+ session.id === props.selectedId && "bg-og-surface-2"
1458
+ ),
1459
+ children: [
1460
+ /* @__PURE__ */ jsx(
1461
+ "span",
1462
+ {
1463
+ className: cn(
1464
+ "size-1.5 rounded-full",
1465
+ session.lifecycle === "active" ? "bg-og-status-running" : "bg-og-muted"
1466
+ )
1467
+ }
1468
+ ),
1469
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
1470
+ /* @__PURE__ */ jsx("span", { className: "block truncate text-og-control text-og-fg", children: session.name }),
1471
+ /* @__PURE__ */ jsxs("span", { className: "block truncate text-og-xs text-og-subtle", children: [
1472
+ platformLabel(session),
1473
+ " \xB7 ",
1474
+ placementLabel(session)
1475
+ ] })
1476
+ ] }),
1477
+ interventionCount > 0 ? /* @__PURE__ */ jsx("span", { className: "rounded-full bg-og-status-waiting/10 px-1.5 py-0.5 text-[10px] font-medium text-og-status-waiting", children: interventionCount }) : null
1478
+ ]
1479
+ },
1480
+ session.id
1481
+ );
1482
+ })
1483
+ ] });
1484
+ }
1485
+ function MenuButton(props) {
1486
+ return /* @__PURE__ */ jsx(
1487
+ "button",
1488
+ {
1489
+ type: "button",
1490
+ onClick: props.onClick,
1491
+ disabled: props.disabled,
1492
+ 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",
1493
+ children: props.children
1494
+ }
1495
+ );
1496
+ }
1497
+ function ComputerTargetRail(props) {
1498
+ const visualTargets = props.targets.filter(isRenderableComputerView);
1499
+ return /* @__PURE__ */ jsxs(
1500
+ "div",
1501
+ {
1502
+ className: "flex h-10 shrink-0 items-center gap-1 overflow-x-auto border-b border-og-border bg-og-surface-0 px-2",
1503
+ "aria-label": "Desktop views",
1504
+ children: [
1505
+ visualTargets.map((target) => /* @__PURE__ */ jsxs(
1506
+ "button",
1507
+ {
1508
+ type: "button",
1509
+ onClick: () => props.onSelect(target.id),
1510
+ className: cn(
1511
+ "flex h-7 max-w-56 shrink-0 items-center gap-1.5 rounded-og-sm border px-2 text-og-control transition",
1512
+ target.id === props.selectedTargetId ? "border-og-border-strong bg-og-surface-2 text-og-fg" : "border-transparent text-og-muted hover:bg-og-surface-1 hover:text-og-fg"
1513
+ ),
1514
+ "aria-pressed": target.id === props.selectedTargetId,
1515
+ children: [
1516
+ target.kind === "screen" ? /* @__PURE__ */ jsx(MonitorIcon, { className: "size-3.5 shrink-0" }) : /* @__PURE__ */ jsx(PanelsTopLeftIcon, { className: "size-3.5 shrink-0" }),
1517
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: target.title || target.applicationId || target.kind }),
1518
+ target.focused ? /* @__PURE__ */ jsx(
1519
+ "span",
1520
+ {
1521
+ className: "size-1.5 shrink-0 rounded-full bg-og-status-running",
1522
+ "aria-label": "Focused"
1523
+ }
1524
+ ) : null
1525
+ ]
1526
+ },
1527
+ target.id
1528
+ )),
1529
+ props.loading ? /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "ml-1 size-3.5 animate-spin text-og-muted" }) : null,
1530
+ !props.loading && visualTargets.length === 0 ? /* @__PURE__ */ jsx("span", { className: "text-og-xs text-og-subtle", children: "Waiting for apps and screens\u2026" }) : null
1531
+ ]
1532
+ }
1533
+ );
1534
+ }
1535
+ function isRenderableComputerView(target) {
1536
+ if (target.kind === "screen") return true;
1537
+ if (target.kind !== "window" || !target.bounds) return false;
1538
+ return target.bounds.width >= 160 && target.bounds.height >= 90;
1539
+ }
1540
+ function isComputerFrameTarget(target) {
1541
+ return target.kind === "window" || target.kind === "screen";
1542
+ }
1543
+ function ComputerLifecyclePanel(props) {
1544
+ const generationLoss = isAttachedChromeGenerationLoss(props.session);
1545
+ const failed = ["failed", "lost", "repair_required"].includes(props.session.lifecycle);
1546
+ const retryCreatesSession = ["failed", "lost"].includes(props.session.lifecycle);
1547
+ return /* @__PURE__ */ jsx("div", { className: "grid min-h-0 flex-1 place-items-center bg-og-bg p-6", children: /* @__PURE__ */ jsxs("div", { className: "max-w-sm text-center", children: [
1548
+ failed ? /* @__PURE__ */ jsx(CircleAlertIcon, { className: "mx-auto size-5 text-og-status-error" }) : /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "mx-auto size-5 animate-spin text-og-muted" }),
1549
+ /* @__PURE__ */ jsx("p", { className: "mt-3 text-og-menu font-medium text-og-fg", children: generationLoss ? "Chrome reconnected\u2014open a fresh browser/desktop." : failed ? "Desktop needs attention" : lifecycleLabel(props.session.lifecycle) }),
1550
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-og-control leading-5 text-og-muted", children: generationLoss ? "This Chrome profile is live again, but the previous desktop cannot move to the new connection. Use Browser \u2192 New browser \u2192 Connected Chrome." : computerFailureMessage(props.session) ?? "The desktop is being prepared on its placement. It will appear here when ready." }),
1551
+ failed && !generationLoss ? /* @__PURE__ */ jsxs(
1552
+ "button",
1553
+ {
1554
+ type: "button",
1555
+ onClick: () => retryCreatesSession ? props.onRetry() : void props.onRefresh(),
1556
+ disabled: props.creating,
1557
+ 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",
1558
+ children: [
1559
+ props.creating ? /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-3.5 animate-spin" }) : /* @__PURE__ */ jsx(RotateCcwIcon, { className: "size-3.5" }),
1560
+ retryCreatesSession ? "Try again" : "Check again"
1561
+ ]
1562
+ }
1563
+ ) : null
1564
+ ] }) });
1565
+ }
1566
+ function ComputerViewport(props) {
1567
+ const canvasRef = useRef4(null);
1568
+ const inputRef = useRef4(null);
1569
+ const pointerStartRef = useRef4(null);
1570
+ const clickTimerRef = useRef4(null);
1571
+ const lastClickRef = useRef4(null);
1572
+ const wheelRef = useRef4(null);
1573
+ const pendingTextRef = useRef4(null);
1574
+ const actionRef = useRef4(props.onAction);
1575
+ const readClipboardRef = useRef4(props.onReadClipboard);
1576
+ const errorRef = useRef4(props.onError);
1577
+ const actionTailRef = useRef4(Promise.resolve());
1578
+ const queuedFrameRef = useRef4(null);
1579
+ const decodingFrameRef = useRef4(false);
1580
+ const mountedRef = useRef4(true);
1581
+ actionRef.current = props.onAction;
1582
+ readClipboardRef.current = props.onReadClipboard;
1583
+ errorRef.current = props.onError;
1584
+ const rawInputEnabled = !props.backgroundActions || props.target?.kind === "screen" || props.target?.focused === true;
1585
+ const paintQueuedFrames = useCallback4(() => {
1586
+ if (decodingFrameRef.current) return;
1587
+ decodingFrameRef.current = true;
1588
+ void (async () => {
1589
+ try {
1590
+ while (mountedRef.current) {
1591
+ const frame = queuedFrameRef.current;
1592
+ queuedFrameRef.current = null;
1593
+ if (!frame) break;
1594
+ let objectUrl = null;
1595
+ try {
1596
+ const blob = new Blob([frame.data.slice().buffer], { type: frame.mediaType });
1597
+ if (typeof createImageBitmap === "function") {
1598
+ const bitmap = await createImageBitmap(blob);
1599
+ try {
1600
+ const canvas2 = canvasRef.current;
1601
+ if (mountedRef.current && canvas2) {
1602
+ paintCanvas(canvas2, bitmap, frame.width, frame.height);
1603
+ }
1604
+ } finally {
1605
+ bitmap.close();
1606
+ }
1607
+ continue;
1608
+ }
1609
+ objectUrl = URL.createObjectURL(blob);
1610
+ const image = await loadImage(objectUrl);
1611
+ const canvas = canvasRef.current;
1612
+ if (mountedRef.current && canvas) {
1613
+ paintCanvas(canvas, image, frame.width, frame.height);
1614
+ }
1615
+ } catch (cause) {
1616
+ if (mountedRef.current) errorRef.current(cause);
1617
+ } finally {
1618
+ if (objectUrl) URL.revokeObjectURL(objectUrl);
1619
+ }
1620
+ }
1621
+ } finally {
1622
+ decodingFrameRef.current = false;
1623
+ if (mountedRef.current && queuedFrameRef.current) paintQueuedFrames();
1624
+ }
1625
+ })();
1626
+ }, []);
1627
+ useEffect4(() => {
1628
+ mountedRef.current = true;
1629
+ return () => {
1630
+ mountedRef.current = false;
1631
+ queuedFrameRef.current = null;
1632
+ };
1633
+ }, []);
1634
+ useEffect4(() => {
1635
+ if (!props.frame) return;
1636
+ queuedFrameRef.current = props.frame;
1637
+ paintQueuedFrames();
1638
+ }, [paintQueuedFrames, props.frame]);
1639
+ useEffect4(
1640
+ () => () => {
1641
+ if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
1642
+ if (wheelRef.current?.timer) clearTimeout(wheelRef.current.timer);
1643
+ if (pendingTextRef.current?.timer) clearTimeout(pendingTextRef.current.timer);
1644
+ },
1645
+ []
1646
+ );
1647
+ const enqueue = useCallback4(
1648
+ (action, frame, after) => {
1649
+ actionTailRef.current = actionTailRef.current.catch(() => void 0).then(async () => {
1650
+ await actionRef.current(action, frame);
1651
+ await after?.();
1652
+ }).catch((cause) => errorRef.current(cause));
1653
+ },
1654
+ []
1655
+ );
1656
+ const point = useCallback4(
1657
+ (frame, clientX, clientY) => computerPoint(canvasRef.current, frame, clientX, clientY),
1658
+ []
1659
+ );
1660
+ const flushPendingText = useCallback4(() => {
1661
+ const pending = pendingTextRef.current;
1662
+ if (!pending) return;
1663
+ clearTimeout(pending.timer);
1664
+ pendingTextRef.current = null;
1665
+ enqueue({ type: "keyboard", action: "type", value: pending.text }, null);
1666
+ }, [enqueue]);
1667
+ const flushPendingClick = useCallback4(() => {
1668
+ const pending = lastClickRef.current;
1669
+ if (!pending) return;
1670
+ if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
1671
+ clickTimerRef.current = null;
1672
+ lastClickRef.current = null;
1673
+ enqueue(
1674
+ {
1675
+ type: "pointer",
1676
+ frameId: pending.frame.frameId,
1677
+ action: "click",
1678
+ x: pending.x,
1679
+ y: pending.y
1680
+ },
1681
+ pending.frame
1682
+ );
1683
+ }, [enqueue]);
1684
+ const pointerDown = (event) => {
1685
+ if (!props.frame || props.mutating || !rawInputEnabled || event.button !== 0) return;
1686
+ flushPendingText();
1687
+ pointerStartRef.current = {
1688
+ x: event.clientX,
1689
+ y: event.clientY,
1690
+ pointerId: event.pointerId,
1691
+ frame: props.frame
1692
+ };
1693
+ event.currentTarget.setPointerCapture?.(event.pointerId);
1694
+ inputRef.current?.focus({ preventScroll: true });
1695
+ };
1696
+ const pointerUp = (event) => {
1697
+ if (!rawInputEnabled) return;
1698
+ const start = pointerStartRef.current;
1699
+ pointerStartRef.current = null;
1700
+ if (!start || start.pointerId !== event.pointerId) return;
1701
+ const from = point(start.frame, start.x, start.y);
1702
+ const to = point(start.frame, event.clientX, event.clientY);
1703
+ if (!from || !to) return;
1704
+ if (Math.hypot(event.clientX - start.x, event.clientY - start.y) > 5) {
1705
+ flushPendingClick();
1706
+ enqueue(
1707
+ {
1708
+ type: "pointer",
1709
+ frameId: start.frame.frameId,
1710
+ action: "drag",
1711
+ x: from.x,
1712
+ y: from.y,
1713
+ endX: to.x,
1714
+ endY: to.y
1715
+ },
1716
+ start.frame
1717
+ );
1718
+ return;
1719
+ }
1720
+ const now = Date.now();
1721
+ const previous = lastClickRef.current;
1722
+ if (previous && now - previous.at < 280 && Math.hypot(previous.x - to.x, previous.y - to.y) < 6 && sameFrameFence(previous.frame, start.frame)) {
1723
+ if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
1724
+ clickTimerRef.current = null;
1725
+ lastClickRef.current = null;
1726
+ enqueue(
1727
+ {
1728
+ type: "pointer",
1729
+ frameId: start.frame.frameId,
1730
+ action: "double_click",
1731
+ x: to.x,
1732
+ y: to.y
1733
+ },
1734
+ start.frame
1735
+ );
1736
+ return;
1737
+ }
1738
+ lastClickRef.current = { at: now, x: to.x, y: to.y, frame: start.frame };
1739
+ clickTimerRef.current = setTimeout(() => {
1740
+ clickTimerRef.current = null;
1741
+ lastClickRef.current = null;
1742
+ enqueue(
1743
+ {
1744
+ type: "pointer",
1745
+ frameId: start.frame.frameId,
1746
+ action: "click",
1747
+ x: to.x,
1748
+ y: to.y
1749
+ },
1750
+ start.frame
1751
+ );
1752
+ }, 280);
1753
+ };
1754
+ const contextMenu = (event) => {
1755
+ event.preventDefault();
1756
+ if (!rawInputEnabled) return;
1757
+ const frame = props.frame;
1758
+ if (!frame) return;
1759
+ flushPendingText();
1760
+ flushPendingClick();
1761
+ const at = point(frame, event.clientX, event.clientY);
1762
+ if (at) {
1763
+ enqueue(
1764
+ {
1765
+ type: "pointer",
1766
+ frameId: frame.frameId,
1767
+ action: "click",
1768
+ x: at.x,
1769
+ y: at.y,
1770
+ button: "right"
1771
+ },
1772
+ frame
1773
+ );
1774
+ }
1775
+ };
1776
+ const wheel = (event) => {
1777
+ if (!rawInputEnabled) return;
1778
+ const frame = props.frame;
1779
+ if (!frame) return;
1780
+ const at = point(frame, event.clientX, event.clientY);
1781
+ if (!at) return;
1782
+ flushPendingText();
1783
+ flushPendingClick();
1784
+ event.preventDefault();
1785
+ const pending = wheelRef.current;
1786
+ if (pending?.timer) clearTimeout(pending.timer);
1787
+ wheelRef.current = {
1788
+ x: at.x,
1789
+ y: at.y,
1790
+ deltaX: (pending && sameFrameFence(pending.frame, frame) ? pending.deltaX : 0) + event.deltaX,
1791
+ deltaY: (pending && sameFrameFence(pending.frame, frame) ? pending.deltaY : 0) + event.deltaY,
1792
+ frame,
1793
+ timer: setTimeout(() => {
1794
+ const batch = wheelRef.current;
1795
+ wheelRef.current = null;
1796
+ if (batch) {
1797
+ enqueue(
1798
+ {
1799
+ type: "pointer",
1800
+ frameId: batch.frame.frameId,
1801
+ action: "scroll",
1802
+ x: batch.x,
1803
+ y: batch.y,
1804
+ deltaX: batch.deltaX,
1805
+ deltaY: batch.deltaY
1806
+ },
1807
+ batch.frame
1808
+ );
1809
+ }
1810
+ }, 45)
1811
+ };
1812
+ };
1813
+ const keyDown = (event) => {
1814
+ if (!rawInputEnabled) return;
1815
+ const command = event.metaKey || event.ctrlKey;
1816
+ if (props.clipboardEnabled && command && !event.altKey && ["c", "v"].includes(event.key.toLowerCase())) {
1817
+ flushPendingText();
1818
+ return;
1819
+ }
1820
+ const key = computerKey(event);
1821
+ if (!key) return;
1822
+ event.preventDefault();
1823
+ flushPendingText();
1824
+ flushPendingClick();
1825
+ enqueue({ type: "keyboard", action: "press", value: key }, null);
1826
+ };
1827
+ const copy = (event) => {
1828
+ if (!props.clipboardEnabled || !rawInputEnabled) return;
1829
+ event.preventDefault();
1830
+ flushPendingText();
1831
+ flushPendingClick();
1832
+ enqueue({ type: "clipboard", operation: "copy" }, null, async () => {
1833
+ const clipboard = await readClipboardRef.current();
1834
+ if (!clipboard.text) return;
1835
+ if (!await copyTextToClipboard(clipboard.text)) {
1836
+ throw new Error("Desktop text could not be copied to the local clipboard");
1837
+ }
1838
+ });
1839
+ };
1840
+ const paste = (event) => {
1841
+ if (!props.clipboardEnabled || !rawInputEnabled) return;
1842
+ event.preventDefault();
1843
+ flushPendingText();
1844
+ flushPendingClick();
1845
+ const text = event.clipboardData.getData("text/plain");
1846
+ enqueue({ type: "clipboard", operation: "write", text }, null, async () => {
1847
+ assertExactComputerClipboard(await readClipboardRef.current(), text);
1848
+ await actionRef.current({ type: "clipboard", operation: "paste" }, null);
1849
+ });
1850
+ };
1851
+ const input = (value) => {
1852
+ if (!value || !rawInputEnabled) return;
1853
+ flushPendingClick();
1854
+ const pending = pendingTextRef.current;
1855
+ if (pending) {
1856
+ clearTimeout(pending.timer);
1857
+ pending.text += value;
1858
+ pending.timer = setTimeout(flushPendingText, 16);
1859
+ } else {
1860
+ pendingTextRef.current = {
1861
+ text: value,
1862
+ timer: setTimeout(flushPendingText, 16)
1863
+ };
1864
+ }
1865
+ if (inputRef.current) inputRef.current.value = "";
1866
+ };
1867
+ const showCanvas = props.frame !== null && !props.machineLocked;
1868
+ return /* @__PURE__ */ jsxs("div", { className: "relative min-h-0 flex-1 overflow-hidden bg-black", children: [
1869
+ /* @__PURE__ */ jsx(
1870
+ "canvas",
1871
+ {
1872
+ ref: canvasRef,
1873
+ className: cn(
1874
+ "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",
1875
+ !showCanvas && "invisible",
1876
+ !rawInputEnabled && "cursor-default"
1877
+ ),
1878
+ onPointerDown: pointerDown,
1879
+ onPointerUp: pointerUp,
1880
+ onPointerCancel: () => {
1881
+ pointerStartRef.current = null;
1882
+ },
1883
+ onContextMenu: contextMenu,
1884
+ onWheel: wheel,
1885
+ "aria-label": `Interactive ${props.target?.kind ?? "desktop"} view`
1886
+ }
1887
+ ),
1888
+ /* @__PURE__ */ jsx(
1889
+ "textarea",
1890
+ {
1891
+ ref: inputRef,
1892
+ defaultValue: "",
1893
+ onInput: (event) => input(event.currentTarget.value),
1894
+ onKeyDown: keyDown,
1895
+ onCopy: copy,
1896
+ onPaste: paste,
1897
+ className: "pointer-events-none absolute left-1/2 top-1/2 size-px resize-none overflow-hidden opacity-0",
1898
+ "aria-label": "Desktop keyboard input",
1899
+ autoCapitalize: "off",
1900
+ autoCorrect: "off",
1901
+ spellCheck: false,
1902
+ disabled: !rawInputEnabled
1903
+ }
1904
+ ),
1905
+ !showCanvas ? /* @__PURE__ */ jsx(
1906
+ ComputerViewportFallback,
1907
+ {
1908
+ observation: props.observation,
1909
+ machineLocked: props.machineLocked,
1910
+ connectionState: props.connectionState,
1911
+ error: props.connectionError,
1912
+ onAction: (action) => enqueue(action, null),
1913
+ onReconnect: props.onReconnect
1914
+ }
1915
+ ) : null,
1916
+ 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: [
1917
+ /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-3 animate-spin" }),
1918
+ " Acting"
1919
+ ] }) : null,
1920
+ showCanvas && !rawInputEnabled ? /* @__PURE__ */ jsxs("div", { className: "absolute bottom-3 left-1/2 flex -translate-x-1/2 items-center gap-2 rounded-full border border-white/10 bg-black/70 py-1 pl-3 pr-1 text-[11px] text-white/80 backdrop-blur", children: [
1921
+ /* @__PURE__ */ jsx("span", { children: "Background view \xB7 use native controls" }),
1922
+ props.target ? /* @__PURE__ */ jsx(
1923
+ "button",
1924
+ {
1925
+ type: "button",
1926
+ disabled: props.mutating,
1927
+ onClick: () => enqueue({ type: "focus", targetId: props.target.id }, null),
1928
+ className: "rounded-full bg-white/10 px-2 py-0.5 font-medium text-white transition hover:bg-white/20 disabled:opacity-50",
1929
+ children: "Control directly"
1930
+ }
1931
+ ) : null
1932
+ ] }) : null
1933
+ ] });
1934
+ }
1935
+ function assertExactComputerClipboard(clipboard, expected) {
1936
+ if (clipboard.truncated || clipboard.text !== expected) {
1937
+ throw new Error("Desktop clipboard did not accept the exact pasted text");
1938
+ }
1939
+ }
1940
+ function ComputerViewportFallback(props) {
1941
+ if (props.machineLocked) {
1942
+ return /* @__PURE__ */ jsx("div", { className: "absolute inset-0 grid place-items-center bg-og-bg p-6", children: /* @__PURE__ */ jsxs("div", { className: "max-w-sm text-center", children: [
1943
+ /* @__PURE__ */ jsx(LockKeyholeIcon, { className: "mx-auto size-6 text-og-muted" }),
1944
+ /* @__PURE__ */ jsx("p", { className: "mt-3 text-og-menu font-medium text-og-fg", children: "Machine locked" }),
1945
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-og-control leading-5 text-og-muted", children: "Live frames and input stay private until someone unlocks the machine." })
1946
+ ] }) });
1947
+ }
1948
+ const controlFailure = interactionControlFailureFromError(props.error);
1949
+ const interactive = semanticNodes(props.observation).filter((node) => semanticAction(node) !== null).slice(0, 10);
1950
+ 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: [
1951
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1952
+ props.error ? /* @__PURE__ */ jsx(CircleAlertIcon, { className: "size-4 text-og-status-error" }) : /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-4 animate-spin text-og-muted" }),
1953
+ /* @__PURE__ */ jsx("p", { className: "text-og-menu font-medium text-og-fg", children: isAttachedChromeGenerationLossError(props.error) ? "Chrome reconnected\u2014open a fresh browser/desktop." : props.error ? "Live view disconnected" : computerConnectionLabel(props.connectionState) })
1954
+ ] }),
1955
+ props.error ? /* @__PURE__ */ jsx("p", { className: "mt-2 text-og-control leading-5 text-og-muted", children: isAttachedChromeGenerationLossError(props.error) ? "Chrome reconnected\u2014open a fresh browser/desktop. Use Browser \u2192 New browser \u2192 Connected Chrome." : controlFailure?.message ?? props.error.message }) : null,
1956
+ interactive.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "mt-3 border-t border-og-border pt-3", children: [
1957
+ /* @__PURE__ */ jsx("p", { className: "mb-2 text-og-xs text-og-subtle", children: "Native controls remain available" }),
1958
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: interactive.map((node) => /* @__PURE__ */ jsx(
1959
+ "button",
1960
+ {
1961
+ type: "button",
1962
+ onClick: () => {
1963
+ const action = semanticAction(node);
1964
+ if (action) props.onAction(action);
1965
+ },
1966
+ 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",
1967
+ children: node.name || node.role
1968
+ },
1969
+ node.ref
1970
+ )) })
1971
+ ] }) : null,
1972
+ props.error && !isAttachedChromeGenerationLossError(props.error) ? /* @__PURE__ */ jsx(
1973
+ "button",
1974
+ {
1975
+ type: "button",
1976
+ onClick: props.onReconnect,
1977
+ className: "mt-3 text-og-control font-medium text-og-accent hover:underline",
1978
+ children: controlFailure?.retryable === false ? "Try again" : "Reconnect"
1979
+ }
1980
+ ) : null
1981
+ ] }) });
1982
+ }
1983
+ function ComputerSemanticPanel(props) {
1984
+ const nodes = semanticNodes(props.observation).slice(0, 100);
1985
+ return /* @__PURE__ */ jsxs("aside", { className: "w-64 shrink-0 overflow-y-auto border-l border-og-border bg-og-surface-0 p-2", children: [
1986
+ /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center gap-1.5 px-1 text-og-xs font-medium uppercase tracking-[0.1em] text-og-subtle", children: [
1987
+ /* @__PURE__ */ jsx(KeyboardIcon, { className: "size-3" }),
1988
+ " Native controls"
1989
+ ] }),
1990
+ nodes.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-1 py-2 text-og-control leading-5 text-og-muted", children: "This view has no accessibility controls. Use the live image instead." }) : /* @__PURE__ */ jsx("div", { className: "space-y-0.5", children: nodes.map((node) => /* @__PURE__ */ jsx(
1991
+ ComputerSemanticControl,
1992
+ {
1993
+ node,
1994
+ mutating: props.mutating,
1995
+ onAction: props.onAction
1996
+ },
1997
+ node.ref
1998
+ )) })
1999
+ ] });
2000
+ }
2001
+ function ComputerSemanticControl(props) {
2002
+ const { node } = props;
2003
+ const editable = node.actions.includes("set_value");
2004
+ const action = semanticAction(node);
2005
+ const observedValue = semanticValue(node) ?? "";
2006
+ const [value, setValue] = useState4(() => observedValue);
2007
+ useEffect4(() => {
2008
+ setValue(observedValue);
2009
+ }, [node.ref, observedValue]);
2010
+ if (editable) {
2011
+ return /* @__PURE__ */ jsxs(
2012
+ "form",
2013
+ {
2014
+ className: "rounded-og-sm px-2 py-1.5 hover:bg-og-surface-2",
2015
+ onSubmit: (event) => {
2016
+ event.preventDefault();
2017
+ props.onAction({
2018
+ type: "semantic",
2019
+ locator: { kind: "ref", ref: node.ref },
2020
+ action: "set_value",
2021
+ value
2022
+ });
2023
+ },
2024
+ children: [
2025
+ /* @__PURE__ */ jsx("label", { className: "block truncate text-[10px] uppercase text-og-subtle", children: node.name || node.identifier || node.role }),
2026
+ /* @__PURE__ */ jsxs("div", { className: "mt-1 flex gap-1", children: [
2027
+ /* @__PURE__ */ jsx(
2028
+ "input",
2029
+ {
2030
+ value,
2031
+ disabled: props.mutating,
2032
+ onChange: (event) => setValue(event.currentTarget.value),
2033
+ className: "min-w-0 flex-1 rounded-og-sm border border-og-border bg-og-surface-1 px-1.5 py-1 text-og-control text-og-fg outline-hidden focus:border-og-accent",
2034
+ "aria-label": `Set ${node.name || node.identifier || node.role}`
2035
+ }
2036
+ ),
2037
+ /* @__PURE__ */ jsx(
2038
+ "button",
2039
+ {
2040
+ type: "submit",
2041
+ disabled: props.mutating,
2042
+ className: "rounded-og-sm border border-og-border bg-og-surface-1 px-2 text-og-control font-medium text-og-fg transition hover:bg-og-surface-2 disabled:opacity-50",
2043
+ children: "Set"
2044
+ }
2045
+ )
2046
+ ] })
2047
+ ]
2048
+ }
2049
+ );
2050
+ }
2051
+ return /* @__PURE__ */ jsxs(
2052
+ "button",
2053
+ {
2054
+ type: "button",
2055
+ disabled: !action || props.mutating,
2056
+ onClick: () => {
2057
+ if (action) props.onAction(action);
2058
+ },
2059
+ className: "flex w-full items-start gap-2 rounded-og-sm px-2 py-1.5 text-left transition hover:bg-og-surface-2 disabled:cursor-default disabled:hover:bg-transparent",
2060
+ children: [
2061
+ /* @__PURE__ */ jsx("span", { className: "mt-0.5 text-[10px] uppercase text-og-subtle", children: node.role }),
2062
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-og-control text-og-fg", children: node.name || semanticValue(node) || node.identifier || "Unnamed control" })
2063
+ ]
2064
+ }
2065
+ );
2066
+ }
2067
+ function ComputerStatusBar(props) {
2068
+ const screen = props.target?.kind === "screen";
2069
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-8 shrink-0 items-center gap-2 border-t border-og-border bg-og-surface-0 px-2 text-og-xs text-og-subtle", children: [
2070
+ /* @__PURE__ */ jsx(
2071
+ "span",
2072
+ {
2073
+ className: cn(
2074
+ "size-1.5 rounded-full",
2075
+ props.connectionState === "live" ? "bg-og-status-running" : "bg-og-muted"
2076
+ )
2077
+ }
2078
+ ),
2079
+ /* @__PURE__ */ jsx("span", { children: props.connectionState === "live" ? "Live" : computerConnectionLabel(props.connectionState) }),
2080
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: screen ? "Full screen \xB7 input may move pointer and focus" : props.session?.capabilities?.backgroundActions ? "Window \xB7 semantic controls stay in the background" : props.target?.kind ?? "Desktop" }),
2081
+ screen ? /* @__PURE__ */ jsx(MousePointer2Icon, { className: "size-3", "aria-hidden": true }) : null,
2082
+ props.refreshing ? /* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-3 animate-spin" }) : null,
2083
+ /* @__PURE__ */ jsxs(
2084
+ "button",
2085
+ {
2086
+ type: "button",
2087
+ onClick: props.onToggleControls,
2088
+ "aria-pressed": props.showControls,
2089
+ className: cn(
2090
+ "rounded px-1.5 py-0.5 transition hover:bg-og-surface-2 hover:text-og-fg",
2091
+ props.showControls && "bg-og-surface-2 text-og-fg"
2092
+ ),
2093
+ children: [
2094
+ "Controls ",
2095
+ props.controlCount
2096
+ ]
2097
+ }
2098
+ )
2099
+ ] });
2100
+ }
2101
+ function ComputerNotice(props) {
2102
+ return /* @__PURE__ */ jsx(
2103
+ "div",
2104
+ {
2105
+ className: cn(
2106
+ "grid h-full place-items-center bg-og-bg text-og-control text-og-muted",
2107
+ props.className
2108
+ ),
2109
+ children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2110
+ props.icon,
2111
+ props.text
2112
+ ] })
2113
+ }
2114
+ );
2115
+ }
2116
+ function semanticNodes(observation) {
2117
+ if (observation?.semantic?.kind !== "snapshot") return [];
2118
+ const result = [];
2119
+ const visit = (node) => {
2120
+ result.push(node);
2121
+ node.children?.forEach(visit);
2122
+ };
2123
+ observation.semantic.roots.forEach(visit);
2124
+ return result;
2125
+ }
2126
+ function semanticAction(node) {
2127
+ const invoke = ["invoke", "press", "click"].some((action) => node.actions.includes(action));
2128
+ if (invoke) {
2129
+ return {
2130
+ type: "semantic",
2131
+ locator: { kind: "ref", ref: node.ref },
2132
+ action: "invoke"
2133
+ };
2134
+ }
2135
+ return null;
2136
+ }
2137
+ function semanticValue(node) {
2138
+ return typeof node.value === "string" ? node.value : null;
2139
+ }
2140
+ function paintCanvas(canvas, image, width, height) {
2141
+ canvas.width = width;
2142
+ canvas.height = height;
2143
+ const context = canvas.getContext("2d", { alpha: false });
2144
+ if (!context) throw new Error("Desktop canvas is unavailable.");
2145
+ context.drawImage(image, 0, 0, width, height);
2146
+ }
2147
+ function loadImage(url) {
2148
+ return new Promise((resolve, reject) => {
2149
+ const image = new Image();
2150
+ image.onload = () => resolve(image);
2151
+ image.onerror = () => reject(new Error("Desktop frame image could not be decoded."));
2152
+ image.src = url;
2153
+ });
2154
+ }
2155
+ function computerPoint(canvas, frame, clientX, clientY) {
2156
+ if (!canvas) return null;
2157
+ const rect = canvas.getBoundingClientRect();
2158
+ if (rect.width <= 0 || rect.height <= 0) return null;
2159
+ return {
2160
+ x: Math.max(0, Math.min(frame.width - 1, (clientX - rect.left) / rect.width * frame.width)),
2161
+ y: Math.max(0, Math.min(frame.height - 1, (clientY - rect.top) / rect.height * frame.height))
2162
+ };
2163
+ }
2164
+ function computerKey(event) {
2165
+ if (["Alt", "AltGraph", "Control", "Meta", "Shift"].includes(event.key)) return null;
2166
+ const special = /* @__PURE__ */ new Set([
2167
+ "Enter",
2168
+ "Tab",
2169
+ "Escape",
2170
+ "Backspace",
2171
+ "Delete",
2172
+ "ArrowUp",
2173
+ "ArrowDown",
2174
+ "ArrowLeft",
2175
+ "ArrowRight",
2176
+ "Home",
2177
+ "End",
2178
+ "PageUp",
2179
+ "PageDown"
2180
+ ]);
2181
+ const modified = event.altKey || event.ctrlKey || event.metaKey;
2182
+ if (!modified && !special.has(event.key)) return null;
2183
+ const parts = [];
2184
+ if (event.ctrlKey) parts.push("Control");
2185
+ if (event.altKey) parts.push("Alt");
2186
+ if (event.metaKey) parts.push("Meta");
2187
+ if (event.shiftKey) parts.push("Shift");
2188
+ parts.push(event.key === " " ? "Space" : event.key);
2189
+ return parts.join("+");
2190
+ }
2191
+ function sameFrameFence(left, right) {
2192
+ return left.frameId === right.frameId && left.computerSessionId === right.computerSessionId && left.targetId === right.targetId && left.targetGeneration === right.targetGeneration;
2193
+ }
2194
+ function isLiveComputer2(session) {
2195
+ return !["ending", "ended", "failed", "lost"].includes(session.lifecycle);
2196
+ }
2197
+ function computerFailureMessage(session) {
2198
+ switch (session.failureCode) {
2199
+ case "machine_locked":
2200
+ return "Unlock the connected Mac, then try again.";
2201
+ case "controller_heartbeat_expired":
2202
+ return "The desktop connection was lost. Try opening it again.";
2203
+ case "controller_transition_expired":
2204
+ return session.placement.kind === "attached_device" ? "Chrome reconnected\u2014open a fresh browser/desktop. Use Browser \u2192 New browser \u2192 Connected Chrome." : "The desktop connection expired. Open a new desktop.";
2205
+ case null:
2206
+ return null;
2207
+ default:
2208
+ return "The desktop could not be opened. Try again.";
2209
+ }
2210
+ }
2211
+ function isAttachedChromeGenerationLoss(session) {
2212
+ return session.lifecycle === "lost" && session.failureCode === "controller_transition_expired" && session.placement.kind === "attached_device";
2213
+ }
2214
+ function isAttachedChromeGenerationLossError(error) {
2215
+ if (!error) return false;
2216
+ return /placement instance changed|controller authority changed/i.test(error.message);
2217
+ }
2218
+ function countInterventions(interventions) {
2219
+ const counts = /* @__PURE__ */ new Map();
2220
+ for (const intervention of interventions) {
2221
+ counts.set(intervention.resourceId, (counts.get(intervention.resourceId) ?? 0) + 1);
2222
+ }
2223
+ return counts;
2224
+ }
2225
+ function interventionTitle(intervention) {
2226
+ switch (intervention.kind) {
2227
+ case "manual_login":
2228
+ return "Sign in needed";
2229
+ case "mfa":
2230
+ return "Verification needed";
2231
+ case "external_action":
2232
+ case "confirmation":
2233
+ return "Action needed";
2234
+ case "other":
2235
+ return "Desktop needs your help";
2236
+ }
2237
+ }
2238
+ function platformLabel(session) {
2239
+ switch (session.platform) {
2240
+ case "macos":
2241
+ return "Mac";
2242
+ case "linux":
2243
+ return "Linux";
2244
+ case "windows":
2245
+ return "Windows";
2246
+ default:
2247
+ return "Desktop";
2248
+ }
2249
+ }
2250
+ function placementLabel(session) {
2251
+ switch (session.placement.kind) {
2252
+ case "sandbox_group":
2253
+ return "Agent sandbox";
2254
+ case "connected_machine":
2255
+ return "Connected machine";
2256
+ case "attached_device":
2257
+ return "Your machine";
2258
+ case "external_provider":
2259
+ return "Cloud desktop";
2260
+ }
2261
+ }
2262
+ function lifecycleLabel(lifecycle) {
2263
+ switch (lifecycle) {
2264
+ case "starting":
2265
+ return "Opening desktop\u2026";
2266
+ case "suspending":
2267
+ return "Pausing desktop\u2026";
2268
+ case "suspended":
2269
+ return "Desktop paused";
2270
+ case "restoring":
2271
+ return "Restoring desktop\u2026";
2272
+ case "ending":
2273
+ return "Closing desktop\u2026";
2274
+ default:
2275
+ return "Connecting to desktop\u2026";
2276
+ }
2277
+ }
2278
+ function computerConnectionLabel(state) {
2279
+ switch (state) {
2280
+ case "attaching":
2281
+ return "Opening desktop view\u2026";
2282
+ case "connecting":
2283
+ return "Connecting\u2026";
2284
+ case "reconnecting":
2285
+ return "Reconnecting\u2026";
2286
+ case "error":
2287
+ return "Disconnected";
2288
+ default:
2289
+ return "Waiting for desktop\u2026";
2290
+ }
2291
+ }
2292
+
2293
+ export {
2294
+ useComputerFrameStream,
2295
+ useComputerSession,
2296
+ useComputerSessions,
2297
+ ComputerViewer,
2298
+ computerKey
2299
+ };
2300
+ //# sourceMappingURL=chunk-QGBJ6GHG.js.map