@opengeni/react 1.0.1 → 2.2.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.
- package/README.md +33 -10
- package/dist/artifacts-spreadsheet.js +1 -1
- package/dist/artifacts.js +1 -1
- package/dist/browser-viewer-GXUICIAG.js +14 -0
- package/dist/{chunk-R3AZOAB5.js → chunk-4MDLEHXL.js} +9 -115
- package/dist/chunk-4MDLEHXL.js.map +1 -0
- package/dist/{chunk-Y7OEDUJ5.js → chunk-5RZN3DD5.js} +1014 -420
- package/dist/chunk-5RZN3DD5.js.map +1 -0
- package/dist/chunk-5YK2ZDIG.js +116 -0
- package/dist/chunk-5YK2ZDIG.js.map +1 -0
- package/dist/chunk-H5YGYNXS.js +329 -0
- package/dist/chunk-H5YGYNXS.js.map +1 -0
- package/dist/{chunk-7WCLAEFV.js → chunk-ICA6R6TW.js} +2 -2
- package/dist/{chunk-7WCLAEFV.js.map → chunk-ICA6R6TW.js.map} +1 -1
- package/dist/{chunk-IGC6A2ZI.js → chunk-JOAVA2TW.js} +589 -217
- package/dist/chunk-JOAVA2TW.js.map +1 -0
- package/dist/{chunk-5UQPNLIO.js → chunk-LTCHJ4MS.js} +9 -9
- package/dist/chunk-LTCHJ4MS.js.map +1 -0
- package/dist/{chunk-C5ZPSSS5.js → chunk-NEXLLTP4.js} +2 -1
- package/dist/{chunk-C5ZPSSS5.js.map → chunk-NEXLLTP4.js.map} +1 -1
- package/dist/chunk-QGBJ6GHG.js +2300 -0
- package/dist/chunk-QGBJ6GHG.js.map +1 -0
- package/dist/{chunk-B6WPZSTT.js → chunk-UOLJWT5Y.js} +78 -16
- package/dist/chunk-UOLJWT5Y.js.map +1 -0
- package/dist/{chunk-MKPIAGVL.js → chunk-W6NFJXSA.js} +8 -19
- package/dist/chunk-W6NFJXSA.js.map +1 -0
- package/dist/{chunk-XBZWOM2K.js → chunk-X4A66EWZ.js} +8 -3
- package/dist/chunk-X4A66EWZ.js.map +1 -0
- package/dist/chunk-X4JAFD5R.js +3907 -0
- package/dist/chunk-X4JAFD5R.js.map +1 -0
- package/dist/{chunk-6KLX2RH5.js → chunk-XSA2QAKP.js} +334 -19
- package/dist/chunk-XSA2QAKP.js.map +1 -0
- package/dist/{chunk-HA46CYQK.js → chunk-XXWH2JLC.js} +792 -206
- package/dist/chunk-XXWH2JLC.js.map +1 -0
- package/dist/client.d.ts +2 -2
- package/dist/components/browser-viewer.d.ts +7 -1
- package/dist/components/computer-viewer.d.ts +4 -1
- package/dist/components/enrollment-consent.d.ts +6 -3
- package/dist/components/file-browser.d.ts +7 -1
- package/dist/components/human-input-form.d.ts +1 -1
- package/dist/components/human-input-surface.d.ts +1 -1
- package/dist/components/machine-card.d.ts +4 -1
- package/dist/components/machines/machine-detail.d.ts +4 -1
- package/dist/components/machines/machine-operation-policy.d.ts +11 -0
- package/dist/components/machines-dashboard.d.ts +3 -1
- package/dist/components/markdown.d.ts +20 -1
- package/dist/components/message-timeline.d.ts +1 -1
- package/dist/components/model-policy-picker.d.ts +2 -0
- package/dist/components/pierre-file.d.ts +1 -1
- package/dist/components/sandbox-files.d.ts +8 -2
- package/dist/components/sandbox-workspace.d.ts +33 -3
- package/dist/components/session-chrome.d.ts +15 -3
- package/dist/components/workspace-dock.d.ts +6 -2
- package/dist/composer.js +5 -5
- package/dist/computer-viewer-ZOP4Q7A7.js +16 -0
- package/dist/computer-viewer-ZOP4Q7A7.js.map +1 -0
- package/dist/hooks/channel-cache.d.ts +23 -0
- package/dist/hooks/shared-machines-list.d.ts +14 -0
- package/dist/hooks/use-attached-browsers.d.ts +2 -1
- package/dist/hooks/use-channels.d.ts +1 -0
- package/dist/hooks/use-composer.d.ts +37 -9
- package/dist/hooks/use-machine-chip.d.ts +3 -3
- package/dist/hooks/use-machines.d.ts +19 -1
- package/dist/hooks/use-sandbox-files.d.ts +1 -0
- package/dist/hooks/use-session-background-commands.d.ts +14 -0
- package/dist/hooks/use-turn-queue.d.ts +12 -0
- package/dist/hooks/use-workspace-sessions.d.ts +1 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +1096 -623
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +29 -181
- package/dist/interaction.js.map +1 -1
- package/dist/lib/interaction-errors.d.ts +3 -0
- package/dist/machines.d.ts +4 -2
- package/dist/machines.js +10 -2
- package/dist/model-policy.d.ts +1 -1
- package/dist/model-policy.js +1 -1
- package/dist/platform-activity-row-BJHUHXAT.js +169 -0
- package/dist/platform-activity-row-BJHUHXAT.js.map +1 -0
- package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-BSKUWHRI.js} +76 -16
- package/dist/queue-surface-implementation-BSKUWHRI.js.map +1 -0
- package/dist/realtime/realtime-control.d.ts +6 -3
- package/dist/realtime.js +36 -14
- package/dist/realtime.js.map +1 -1
- package/dist/session-ui.d.ts +1 -1
- package/dist/session-ui.js +6 -2
- package/dist/session.d.ts +2 -2
- package/dist/session.js +4 -4
- package/dist/timeline/index.d.ts +1 -1
- package/dist/timeline/platform-activity-row.d.ts +15 -0
- package/dist/timeline/screenshot-lightbox.d.ts +1 -1
- package/dist/timeline/shared.d.ts +4 -2
- package/dist/timeline/types.d.ts +48 -5
- package/dist/types/machines.d.ts +2 -2
- package/package.json +3 -3
- package/src/client.ts +12 -1
- package/src/components/artifacts/editable-spreadsheet.tsx +1 -1
- package/src/components/browser-viewer.tsx +361 -58
- package/src/components/chat-composer.tsx +1 -1
- package/src/components/composer.tsx +6 -6
- package/src/components/computer-viewer.tsx +368 -115
- package/src/components/enrollment-consent.tsx +64 -6
- package/src/components/file-browser.tsx +117 -2
- package/src/components/human-input-form.tsx +91 -55
- package/src/components/human-input-surface.tsx +1 -1
- package/src/components/machine-card.tsx +111 -0
- package/src/components/machine-input-display.ts +15 -0
- package/src/components/machines/machine-detail.tsx +47 -0
- package/src/components/machines/machine-operation-policy.tsx +179 -0
- package/src/components/machines-dashboard.tsx +6 -0
- package/src/components/markdown.tsx +174 -20
- package/src/components/message-timeline.tsx +162 -17
- package/src/components/model-policy-picker.tsx +9 -17
- package/src/components/pierre-file.tsx +1 -1
- package/src/components/queue-surface-implementation.tsx +82 -16
- package/src/components/sandbox-files.tsx +135 -19
- package/src/components/sandbox-workspace.tsx +204 -92
- package/src/components/session-chrome.tsx +415 -208
- package/src/components/session-status.tsx +1 -1
- package/src/components/user-message-body.tsx +168 -48
- package/src/components/workspace-dock.tsx +141 -175
- package/src/hooks/channel-cache.ts +104 -0
- package/src/hooks/shared-machines-list.ts +239 -0
- package/src/hooks/use-attached-browsers.ts +5 -1
- package/src/hooks/use-browser-frame-stream.ts +6 -1
- package/src/hooks/use-browser-session.ts +6 -4
- package/src/hooks/use-browser-sessions.ts +7 -1
- package/src/hooks/use-channels.ts +32 -10
- package/src/hooks/use-composer.ts +760 -291
- package/src/hooks/use-computer-frame-stream.ts +14 -9
- package/src/hooks/use-computer-session.ts +16 -14
- package/src/hooks/use-computer-sessions.ts +8 -1
- package/src/hooks/use-last-started-turn-policy.ts +1 -1
- package/src/hooks/use-machine-chip.ts +23 -6
- package/src/hooks/use-machines.ts +104 -6
- package/src/hooks/use-sandbox-files.ts +195 -140
- package/src/hooks/use-sandbox-git.ts +58 -29
- package/src/hooks/use-session-background-commands.ts +56 -0
- package/src/hooks/use-session-capabilities.ts +18 -0
- package/src/hooks/use-turn-queue.ts +111 -12
- package/src/hooks/use-workspace-sessions.ts +5 -1
- package/src/index.ts +26 -4
- package/src/lib/interaction-errors.ts +18 -0
- package/src/machines.ts +9 -1
- package/src/model-policy.ts +10 -2
- package/src/realtime/realtime-control.tsx +53 -17
- package/src/session-context.ts +1 -0
- package/src/session-ui.ts +6 -1
- package/src/session.ts +9 -1
- package/src/timeline/activity-rail.tsx +20 -75
- package/src/timeline/index.ts +4 -0
- package/src/timeline/platform-activity-row.tsx +201 -0
- package/src/timeline/projection.ts +476 -23
- package/src/timeline/screenshot-lightbox.tsx +48 -14
- package/src/timeline/shared.tsx +8 -2
- package/src/timeline/tool-renderers.tsx +104 -6
- package/src/timeline/turn-summary.tsx +6 -1
- package/src/timeline/types.ts +75 -2
- package/src/types/machines.ts +10 -0
- package/styles/compiled.css +115 -104
- package/dist/chunk-5UQPNLIO.js.map +0 -1
- package/dist/chunk-6KLX2RH5.js.map +0 -1
- package/dist/chunk-B6WPZSTT.js.map +0 -1
- package/dist/chunk-HA46CYQK.js.map +0 -1
- package/dist/chunk-IGC6A2ZI.js.map +0 -1
- package/dist/chunk-LLOLUHVG.js +0 -5937
- package/dist/chunk-LLOLUHVG.js.map +0 -1
- package/dist/chunk-MKPIAGVL.js.map +0 -1
- package/dist/chunk-R3AZOAB5.js.map +0 -1
- package/dist/chunk-XBZWOM2K.js.map +0 -1
- package/dist/chunk-Y7OEDUJ5.js.map +0 -1
- package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
- package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
- /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
|